itertools.groupby的意外行为

时间:2016-03-14 15:38:16

标签: python python-3.x python-2.x itertools python-internals

这是观察到的行为:

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 个答案:

答案 0 :(得分:6)

itertools.groupby文档告诉

  

itertools.groupby(iterable, key=None)

     

[...]

     

groupby()的操作类似于Unix中的uniq过滤器。每次键函数的值发生变化时,它都会生成一个中断或新组(这就是为什么通常需要使用相同的键函数对数据进行排序)。这种行为不同于SQL的GROUP BY,它聚合了常见元素而不管它们的输入顺序如何。

     

返回的组本身就是一个迭代器,它与groupby()共享底层的iterable。因为源是共享的,所以当`groupby()对象被提前时,前一个组不再可见。因此,如果稍后需要该数据,应将其存储为列表 [ - ]

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

这是因为_grouper iterator滞后原始迭代器后面的一个项目,这是因为groupby需要先查看一个项目以查看它是属于当前还是下一个组,但它必须以后可以将此项目作为新组的第一项。

然而currkey的{​​{1}}和currvalue属性在original iterator is exhausted重置,因此groupby仍然指向到迭代器的最后一项。

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

currvalue

值得注意的是,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时,StopItertion仍包含前一组的最后一个密钥。现在,当self.currvalue成为y[1]时,第一个会产生list的值,然后才会在底层迭代器上运行self.currvalue (并再次与next()见面。)

即使文档中有Python等价物,其行为与CPython中的权威C代码实现完全相同,但IronPython,Jython和PyPy会产生不同的结果。

答案 1 :(得分: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-generator已完成:

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