itertools.groupby 的意外行为

Unexpected Behavior of itertools.groupby

这是观察到的行为:

In [4]: x = itertools.groupby(range(10), lambda x: True)

In [5]: y = next(x)

In [6]: next(x)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-6-5e4e57af3a97> in <module>()
----> 1 next(x)

StopIteration: 

In [7]: y
Out[7]: (True, <itertools._grouper at 0x10a672e80>)

In [8]: list(y[1])
Out[8]: [9]

list(y[1]) 的预期输出是 [0,1,2,3,4,5,6,7,8,9]

这是怎么回事?

我在 cpython 3.4.2 上看到了这个,但其他人在 cpython 3.5IronPython 2.9.9a0 (2.9.0.0) on Mono 4.0.30319.17020 (64-bit) 上看到了这个。

Jython 2.7.0 和 pypy 上观察到的行为:

Python 2.7.10 (5f8302b8bf9f, Nov 18 2015, 10:46:46)
[PyPy 4.0.1 with GCC 4.8.4]

>>>> x = itertools.groupby(range(10), lambda x: True)
>>>> y = next(x)
>>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>> y
(True, <itertools._groupby object at 0x00007fb1096039a0>)
>>>> list(y[1])
[]

问题是您将所有这些都分组到一个组中,因此在第一个 next 调用之后所有内容都已经分组了:

import itertools
x = itertools.groupby(range(10), lambda x: True)
key, elements = next(x)

但是 elements 是一个生成器,所以你需要立即将它传递到某个结构中,接受一个迭代到 "print" 或 "save" 它,即 list :

print('Key: "{}" with value "{}"'.format(key, list(elements)))

然后你的 range(10) 是空的并且 groupy-generator 完成了:

Key: True with value [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

itertools.groupby 文档告诉我们

itertools.groupby(iterable, key=None)

[...]

The operation of groupby() is similar to the uniq filter in Unix. It generates a break or new group every time the value of the key function changes (which is why it is usually necessary to have sorted the data using the same key function). That behavior differs from SQL’s GROUP BY which aggregates common elements regardless of their input order.

The returned group is itself an iterator that shares the underlying iterable with groupby(). Because the source is shared, when the `groupby() object is advanced, the previous group is no longer visible. So, if that data is needed later, it should be stored as a list [--]

所以最后一段的假设是生成的列表将是空列表[],因为迭代器已经前进,并且满足StopIteration;但在 CPython 中,结果令人惊讶 [9].


这是因为 _grouper iterator 落后于原始迭代器的一项,这是因为 groupby 需要提前查看一项以查看它是属于当前还是下一组,但是它必须能够稍后将此项目生成为新组的第一项。

然而groupbycurrkeycurrvalue属性在original iterator is exhausted不会重置,所以currvalue 仍然指向迭代器的最后一项。

CPython 文档实际上包含这个等效代码,它也具有与 C 版本代码完全相同的行为:

class groupby:
    # [k for k, g in groupby('AAAABBBCCDAABBB')] --> A B C D A B
    # [list(g) for k, g in groupby('AAAABBBCCD')] --> AAAA BBB CC D
    def __init__(self, iterable, key=None):
        if key is None:
            key = lambda x: x
        self.keyfunc = key
        self.it = iter(iterable)
        self.tgtkey = self.currkey = self.currvalue = object()
    def __iter__(self):
        return self
    def __next__(self):
        while self.currkey == self.tgtkey:
            self.currvalue = next(self.it)    # Exit on StopIteration
            self.currkey = self.keyfunc(self.currvalue)
        self.tgtkey = self.currkey
        return (self.currkey, self._grouper(self.tgtkey))
    def _grouper(self, tgtkey):
        while self.currkey == tgtkey:
            yield self.currvalue
            try:
                self.currvalue = next(self.it)
            except StopIteration:
                return
            self.currkey = self.keyfunc(self.currvalue)

值得注意的是 __next__ 找到下一组的第一项,并将它的键存储到 self.currkey 并将其值存储到 self.currvalue。但关键是行

self.currvalue = next(self.it)    # Exit on StopIteration

next 抛出 StopItertionself.currvalue 仍然包含前一组的最后一个键。现在,当 y[1] 变成 list 时,它 首先 产生 self.currvalue 的值,然后才运行 next()底层迭代器(并再次遇到 StopIteration)。


即使文档中有 Python 等效项,其行为与 CPython、IronPython、Jython 和 PyPy 中的权威 C 代码实现完全一样,给出不同的结果。