itertools.groupby的意外行为

6

以下是观察到的行为:

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])
[]
2个回答

6

itertools.groupby 的文档说明:

itertools.groupby(iterable, key=None)

[...]

groupby() 的操作类似于 Unix 中的 uniq 过滤器。每当键函数的值更改时(这就是为什么通常需要使用相同的键函数对数据进行排序),它会生成一个断点或新组。该行为与 SQL 的 GROUP BY 不同,后者会聚合公共元素,而不考虑其输入顺序。

返回的组本身是一个迭代器,它与 groupby() 共享基础可迭代对象。因为源是共享的,所以当 `groupby() object` 被推进时,先前的组将不再可见。因此,如果稍后需要这些数据,则应将其存储为列表 [--]。

因此,最后一段的假设是,生成的列表将是空列表 [],因为迭代器已经被推进并遇到了 StopIteration;但在 CPython 中,结果是出人意料的 [9]


这是因为 _grouper 迭代器 落后于原始迭代器一个项目,这是因为 groupby 需要预先查看一个项目以查看它是否属于当前组或下一组,但它必须能够稍后将此项作为新组的第一项产生。

然而,当原始迭代器耗尽时,groupbycurrkeycurrvalue 属性不会被重置,因此 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抛出StopIteration异常时,self.currvalue仍然包含前一个组的最后一个键。现在,当将y[1]转换为list时,它首先产生self.currvalue的值,然后才在基础迭代器上运行next()(并再次遇到StopIteration)。
尽管文档中有Python等效代码,其行为与CPython、IronPython、Jython和PyPy中的权威C代码实现完全相同,但结果不同。

2
问题在于您将它们全部分组到一个组中,因此,在第一次 next 调用后,所有内容都已经分组了。
import itertools
x = itertools.groupby(range(10), lambda x: True)
key, elements = next(x)

但是elements是一个生成器,因此您需要立即将其传递到接受可迭代对象进行“打印”或“保存”的某个结构中,例如list

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

然后你的range(10)就是空的,groupy生成器也就结束了:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接