问题描述
在使用 zip
转置 itertools.groupby
的结果时,我遇到了一些意外的空列表。实际上我的数据是一堆对象,但为了简单起见,假设我的起始数据是这个列表:
> a = [1,1,2,3,1]
我想对重复项进行分组,所以我使用 itertools.groupby
(先排序,否则 groupby
只会对连续的重复项进行分组):
from itertools import groupby
duplicates = groupby(sorted(a))
这给出了一个 itertools.groupby
对象,当转换为列表时给出
[(1,<itertools._grouper object at 0x7fb3fdd86850>),(2,<itertools._grouper object at 0x7fb3fdd91700>),(3,<itertools._grouper object at 0x7fb3fdce7430>)]
到目前为止,一切都很好。但现在我想转置结果,因此我有一个唯一值列表 [1,3]
,以及每个重复组中的项目列表 [<itertools._grouper object ...>,...]
。为此,我使用了 the solution in this answer 来使用 zip 来“解压缩”:
>>> keys,values = zip(*duplicates)
>>> print(keys)
(1,3)
>>> print(values)
(<itertools._grouper object at 0x7fb3fdd37940>,<itertools._grouper object at 0x7fb3fddfb040>,<itertools._grouper object at 0x7fb3fddfb250>)
但是当我尝试读取 itertools._grouper
对象时,我得到了一堆空列表:
>>> for value in values:
... print(list(value))
...
[]
[]
[]
这是怎么回事?每个 value
不应该包含原始列表中的重复项,即 (1,1)
、(2,2)
和 (3,3)
?
解决方法
要按每个唯一键进行分组以进行重复处理:
import itertools
a = [1,1,2,3,1]
g1 = itertools.groupby(sorted(a))
for k,v in g1:
print(f"Key {k} has",end=" ")
for e in v:
print(e,end=" ")
print()
# Key 1 has 1 1 1 1 1
# Key 2 has 2 2
# Key 3 has 3 3
如果只是为了计算有多少,排序最少:
import itertools
import collections
a = [1,1]
g1 = itertools.groupby(a)
c1 = collections.Counter()
for k,v in g1:
l = len(tuple(v))
c1[k] += l
for k,v in c1.items():
print(f"Element {k} repeated {v} times")
# Element 1 repeated 5 times
# Element 2 repeated 2 times
# Element 3 repeated 2 times
,
啊。多个迭代器都使用同一个底层对象的美妙之处。
groupby
的文档解决了这个问题:
返回的组本身就是一个迭代器,它与 groupby()
共享底层的可迭代对象。因为源是共享的,所以当 groupby()
对象前进时,之前的组不再可见。因此,如果稍后需要该数据,则应将其存储为列表:
groups = []
uniquekeys = []
data = sorted(data,key=keyfunc)
for k,g in groupby(data,keyfunc):
groups.append(list(g)) # Store group iterator as a list
uniquekeys.append(k)
所以最终会发生的情况是,所有 itertools._grouper
对象在您解包之前都被消耗掉了。如果您尝试多次重用任何其他迭代器,您会看到类似的效果。如果您想更好地理解,请查看文档中的下一段,其中显示了 groupby
的内部结构实际是如何工作的。
帮助我理解这一点的部分原因是使用更明显的不可重用迭代器(如文件对象)来工作示例。它有助于摆脱您可以跟踪的底层缓冲区的想法。
一个简单的解决方法是自己使用对象,如文档所建议的:
# This is an iterator over a list:
duplicates = groupby(sorted(a))
# If you convert duplicates to a list,you consume it
# Don't store _grouper objects: consume them yourself:
keys,values = zip(*((key,list(value)) for key,value in duplicates)
正如另一个答案所暗示的那样,您不需要涉及排序的 O(N log N)
解决方案,因为您可以在 O(N)
时间内一次性完成此操作。不过,我建议使用 Counter
来帮助存储列表,而不是使用 defaultdict
:
from collections import defaultdict
result = defaultdict(list)
for item in a:
result[item].append(item)
对于更复杂的对象,您可以使用 key(item)
而不是 item
进行索引。