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

标签 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”)也有意义......解包时,我们消耗 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 对象,因此我暂时忽略它们。因此,有趣的是当您调用 nextgroupby 中会发生什么:

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,它们仍然会指向最后遇到的值(迭代器的最后一项),因为当 PyIter_Next(gbo) 时它只是返回 NULL ->it) == NULL.

完成后,您就有了两个grouper对象。第一个的 tgtvalueFalse,第二个的 tgtvalueTrue。让我们看看当您对这些 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分支并不有趣。对于您的第一个石斑鱼,它会比较 groupertgtkeygroupby 对象,发现它们不同,并立即返回 NULL 。所以你得到了一个空列表。

对于第二个迭代器,tgtkey 是相同的,因此它将返回 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但缩短了。

关于python - `groupby` 的实现特定行为和参数解包,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43905804/

相关文章:

python - 使用pypy2.2时导入Numpypy

python - while循环需要特定的命令才能工作?

python - 将搜索结果限制在谷歌自定义搜索 api 中的特定日期范围内

Python:按名称加载模块

python - 新的 sympy 交叉点用法

python - 如何在 python OrderedDict 上使用字符串键而不是整数进行切片?

python - 如何列出 pypy cffi 的 ffi 中的每个可用标识符?

python - 有没有办法在 Arago 项目上安装 Pypy3?

python-3.x - 什么是 Python 3 `str.__getitem__` 计算复杂度?

python - 列表python的内存大小