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

Implementation specific behavior of `groupby` and argument unpacking

我试图理解我在今天早些时候写的 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") 也是有意义的......解包时,我们也消耗了 b(因为python 需要 寻找之后的内容,以确保它不应该为错误数量的项目抛出 ValueError 来解包)。但是出于某种原因,CPython 版本保留了输入迭代中的最后一个元素……谁能解释为什么会这样?

编辑

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

inputs = ((x > 5, x) for x in range(10))
(_, a), (_, b) = groupby(inputs, key=itemgetter(0))
print(list(a))
print(list(b))

并得到相同的结果...

这是因为 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;

因为在解压 groupby 对象时没有迭代 grouper 对象,所以我暂时忽略它们。所以有趣的是当你在 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->currkeygbo->currvaluegbo->tgtkey 并未设置为 NULL,它们仍将指向最后一个遇到值(迭代器的最后一项)因为它只是 return NULLPyIter_Next(gbo->it) == NULL.

完成后,您将获得两个 grouper 对象。第一个的 tgtvalueFalse,第二个为 True。让我们看看当您在这些 grouper 上调用 next 时会发生什么:

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 分支并不有趣。对于您的第一个石斑鱼,它比较 groupergroupby 对象的 tgtkey,发现它们不同,它会立即 return NULL。所以你得到了一个空列表。

对于第二个迭代器,tgtkey 是相同的,因此它将 return groupby 对象的 currvalue(这是迭代器中最后遇到的值!),但这次它将 groupby 对象的 currvaluecurrkey 设置为 NULL.


切换回 python:如果您的 groupergroupby 中的最后一组具有相同的 tgtkey,就会发生真正有趣的怪癖:

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 中的那个元素根本不属于第一组 - 但因为第一个石斑鱼对象的 tgtkeyFalse 而最后一个 tgtkeyFalse第一条石斑鱼认为它属于第一组。它还使 groupby 对象无效,因此第三组现在为空。


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