`groupby`和参数解包的实现特定行为

时间:2017-05-11 03:00:02

标签: python pypy python-internals

我试图理解我在今天早些时候写过的an answer中发现的一个怪癖。基本上,我正在从包含itertools.groupby的生成器函数中产生组。我发现有趣的是,如果我在赋值的左侧解包生成器,则生成器的最后一个元素仍然存在。例如:

# test_gb.py
from itertools import groupby
from operator import itemgetter

inputs = ((x > 5, x) for x in range(10))

def make_groups(inputs):
    for _, group in groupby(inputs, key=itemgetter(0)):
        yield group

a, b = make_groups(inputs)
print(list(a))
print(list(b))

在Cpython上,结果是:

$ python3 ~/sandbox/test_gb.py 
[]
[(True, 9)]

CPython2.7和CPython3.5就属于这种情况。

在PyPy上,它会导致:

$ pypy ~/sandbox/test_gb.py 
[]
[]

在这两种情况下,第一个空列表(" a")很容易解释 - 来自itertools的组会在下一个元素被消耗后立即消耗需要。由于我们没有将这些值保存在任何地方,因此他们会失去以太。

在我看来,PyPy版本对于第二个空列表(" b")也是有意义的...当解压缩时,我们消耗{{1}也是因为python 需要来查找之后的内容,以确保它不应该为错误数量的项目抛出b来解包。但是出于某种原因,ValueError版本保留了输入可迭代的最后一个元素......任何人都可以解释为什么会这样吗?

修改

这可能或多或少显而易见,但我们也可以将其写成:

CPython

并获得相同的结果......

1 个答案:

答案 0 :(得分:2)

这是因为groupby对象处理簿记,而grouper对象只引用了他们的key和父groupby对象:

typedef struct {
    PyObject_HEAD
    PyObject *it;          /* iterator over the input sequence */
    PyObject *keyfunc;     /* the second argument for the groupby function */
    PyObject *tgtkey;      /* the key for the current "grouper" */
    PyObject *currkey;     /* the key for the current "item" of the iterator*/
    PyObject *currvalue;   /* the plain value of the current "item" */
} groupbyobject;

typedef struct {
    PyObject_HEAD
    PyObject *parent;      /* the groupby object */
    PyObject *tgtkey;      /* the key value for this grouper object. */
} _grouperobject;

由于您在解包grouper对象时未对groupby对象进行迭代,因此我暂时忽略它们。那么当您在groupby上调用next时,static PyObject * groupby_next(groupbyobject *gbo) { PyObject *newvalue, *newkey, *r, *grouper; /* skip to next iteration group */ for (;;) { if (gbo->currkey == NULL) /* pass */; else if (gbo->tgtkey == NULL) break; else { int rcmp; rcmp = PyObject_RichCompareBool(gbo->tgtkey, gbo->currkey, Py_EQ); if (rcmp == 0) break; } newvalue = PyIter_Next(gbo->it); if (newvalue == NULL) return NULL; /* just return NULL, no invalidation of attributes */ newkey = PyObject_CallFunctionObjArgs(gbo->keyfunc, newvalue, NULL); gbo->currkey = newkey; gbo->currvalue = newvalue; } gbo->tgtkey = gbo->currkey; grouper = _grouper_create(gbo, gbo->tgtkey); r = PyTuple_Pack(2, gbo->currkey, grouper); return r; } 会发生什么有趣的事情:

gbo->currkey

我删除了所有不相关的异常处理代码并删除或简化了纯引用计数内容。这里有趣的是,当你到达迭代器的末尾时,gbo->currvaluegbo->tgtkeyNULL都没有设置为return NULL,他们仍会指向最后遇到的值(迭代器的最后一项),因为PyIter_Next(gbo->it) == NULL时只有grouper

完成此操作后,您将拥有两个tgtvalue个对象。第一个是False True,第二个是next。让我们来看看当您在这些grouper上致电static PyObject * _grouper_next(_grouperobject *igo) { groupbyobject *gbo = (groupbyobject *)igo->parent; PyObject *newvalue, *newkey, *r; int rcmp; if (gbo->currvalue == NULL) { /* removed because irrelevant. */ } rcmp = PyObject_RichCompareBool(igo->tgtkey, gbo->currkey, Py_EQ); if (rcmp <= 0) /* got any error or current group is end */ return NULL; r = gbo->currvalue; /* this accesses the last value of the groupby object */ gbo->currvalue = NULL; gbo->currkey = NULL; return r; } 时会发生什么:

currvalue

所以请记住NULL if,因此第一个tgtkey分支并不感兴趣。对于您的第一个石斑鱼,它会比较groupergroupby对象的return NULL,看到它们不同,它会立即tgtkey。所以你有一个空列表。

对于第二个迭代器,currvalue是相同的,因此将返回groupby对象的currvalue (这是最后遇到的值)迭代器!),但这一次它会将currkey对象的groupbyNULL设置为grouper

切换回python:如果tgtkey的{​​{1}}与groupby中的最后一组import itertools >>> inputs = [(x > 5, x) for x in range(10)] + [(False, 10)] >>> (_, g1), (_, g2), (_, g3) = itertools.groupby(inputs, key=lambda x: x[0]) >>> list(g1) [(False, 10)] >>> list(g3) [] 相同,则会发生真正有趣的怪癖:

g1

tgtkey中的一个元素根本不属于第一个组 - 但是因为第一个石斑鱼对象的Falsetgtkey而最后一个Falsegroupby第一个石斑鱼人认为它属于第一组。它还使clientSchema.methods.paginate = function(pageNo, callback){ var limit = 10; var skip = pageNo * (limit - 1); var totalCount; //count documents this.count({}, function(err, count)){ if(err){ totalCount = 0; } else{ totalCount = count; } } if(totalCount == 0){ return callback('No Document in Database..', null); } //get paginated documents this.find().skip(skip).limit(limit).exec(function(err, docs){ if(err){ return callback('Error Occured', null); } else if(!docs){ return callback('Docs Not Found', null); } else{ var result = { "totalRecords" : totalCount, "page": pageNo, "nextPage": pageNo + 1, "result": docs }; return callback(null, result); } }); }); const Client = module.exports = mongoose.model('clients',clientSchema); 对象无效,因此第三组现在为空。

所有代码均来自the Python source code但已缩短。