首页 文章

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

提问于
浏览
2

我试图理解我在今天早些时候写过的_1658709中发现的一个怪癖 . 基本上,我正在从包含 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需要查找's after to make sure that it shouldn'为错误的项目数量抛出 ValueError 打开包装) . 出于某种原因, CPython 版本保留了输入可迭代的最后一个元素......任何人都可以解释为什么会这样?

Edit

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

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

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

1 回答

  • 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;
    

    因为你在解压缩 groupby 对象时没有迭代 grouper 对象,所以'll ignore them for now. So what'有趣的是 groupby 当你在它上面调用_1658734时会发生什么:

    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 NULL PyIter_Next(gbo->it) == NULL .

    完成后,你有两个 grouper 对象 . 第一个将 tgtvalue False ,第二个 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;
    }
    

    所以请记住 currvaluenot NULL ,所以第一个 if 分支并不有意思 . 对于你的第一个石斑鱼,它会比较 groupergroupby 对象的 tgtkey ,看到它们不同,它会立即 return NULL . 所以你有一个空列表 .

    对于第二个迭代器, tgtkey 是相同的,因此它将 return the currvalue of the groupby object (这是迭代器中最后遇到的值!),但这次它将 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但缩短了 .

相关问题