python groupby 的行为?

16 投票
3 回答
9993 浏览
提问于 2025-04-16 18:57
>>from itertools import groupby
>>keyfunc = lambda x : x > 500
>>obj = dict(groupby(range(1000), keyfunc))
>>list(obj[True])
[999]
>>list(obj[False])
[]

使用range(1000)时,默认情况下是按顺序排列的,条件是 (x > 500)。
我原本以为从0到999的数字会根据条件 (x > 500) 被分组到一个字典里。但是结果字典里只有999这个数字。
其他的数字都去哪了呢? 有人能解释一下这是怎么回事吗?

3 个回答

4

你缺少的一个点是,groupby函数会遍历你给定的range(1000),所以它会返回1000个值。而你只保存了最后一个,也就是999。你需要做的是遍历这些返回的值,并把它们保存到你的字典里:

dictionary = {}
keyfunc = lambda x : x > 500
for k, g in groupby(range(1000), keyfunc):
    dictionary[k] = list(g)

这样你就能得到预期的输出了:

{False: [0, 1, 2, ...], True: [501, 502, 503, ...]}

想了解更多信息,可以查看Python文档中关于 itertools groupby 的部分。

11

groupby这个迭代器会返回一对数据:一个是分组函数的结果,另一个是与同一个“外部”迭代器相关的新迭代器。当你对groupby返回的迭代器使用dict()时,如果没有消耗这个“内部”迭代器,groupby就会为你推进“外部”迭代器。你需要明白的是,groupby函数并不是直接作用于一个序列,而是把任何序列转换成一个迭代器。

也许用一些比喻来解释会更好。请跟着我一起想象一个取水的场景。

想象迭代器就像一个人从井里用桶取水。他有无限的桶可以使用,但井里的水是有限的。每当你向这个人请求一桶水时,他就会从井里取出一桶水并递给你。

groupby的情况下,你在这个取水的队伍中插入了另一个人。这个人并不会立即递桶给你。每当你请求一桶水时,他会把你给他的指令的结果和一个的人一起传递给你,这个人会通过groupby的人把桶递给任何请求的人,只要他们的指令结果相同。如果指令的结果改变了,groupby就会停止递桶。因此,groupby桶,groupby再把这些桶传给每个组的人,比如group Agroup B等等。

在你的例子中,水是有编号的,但从井里最多只能取出1000桶。接下来,当你把groupby传给dict()时,会发生以下情况:

  1. 你的dict()请求groupby一桶水。此时,groupby向井里的人请求一桶水,记住你给的指令结果,并保留这桶水。然后它会把指令结果(False)和一个新的人group A传给dict()。这个结果被存储为键,而group A这个想要取水的人被存储为值。不过,这个人还没有请求水,因为没有人要求他

  2. 你的dict()再次请求groupby一桶水。groupby根据指令去寻找下一个结果改变的桶。它还在保留第一桶水,因为没有人请求,所以它扔掉了这桶水。接着,它向井里请求下一桶水,结果还是一样,所以这桶水也被扔掉了!更多的水洒在地上,接下来的499桶也一样。只有当第501桶水传递过来时,结果才会改变,这时groupby会找到另一个人(group B)来接收指令和新的结果True,然后把这两个传给dict()

  3. 你的dict()True存储为键,把group B存储为值。group B什么也不做,因为没有人向他请求水。

  4. 你的dict()请求另一桶水。groupby又洒了更多的水,直到它手里拿着第999桶,井里的人耸耸肩说井里没水了。groupby告诉dict()井里没水了,不能再请求了。它仍然保留着第999桶水,因为它从来不需要为下一桶水腾出空间。

  5. 这时你来找dict(),请求与键True相关的东西,也就是group Bgroup B会去请求groupby能得到的所有桶。group B回到groupby那里,只有一桶水,就是第999桶,而这桶的指令结果正好符合group B的要求。所以group B把这桶水给了list(),然后耸耸肩表示没有更多的桶了,因为groupby告诉他没有了。

  6. 接着你请求dict()与键False相关的人,也就是group A。这时,groupby已经没有东西可以给了,井干了,他正站在一滩999桶水中,水面上漂浮着编号。你的第二个list()什么也得不到。

这个故事的道理是什么呢?和groupby交谈时,最好立即请求所有的水桶,因为如果不这样做,他会把水洒得一地都是!迭代器就像《幻想曲》中的扫帚,努力地移动水却不懂其中的道理,如果你不知道如何控制它们,最好希望水能用完。

这里有段代码可以实现你期待的效果(少一点水以防洪水):

>>> from itertools import groupby
>>> keyfunc = lambda x : x > 5
>>> obj = dict((k, list(v)) for k, v in groupby(range(10), keyfunc))
>>> obj(True)
[0, 1, 2, 3, 4, 5]
>>> obj(False)
[6, 7, 8, 9]
25

来自文档

返回的组本身是一个迭代器,它和groupby()共享底层的可迭代对象。因为这个源是共享的,当groupby()对象向前移动时,之前的组就不再可见了。所以,如果后面还需要这些数据,就应该把它们存储为一个列表[.]

而你是在obj中存储迭代器,并在后面再使用它们。

In [21]: dict((k, list(g)) for k, g in groupby(range(10), lambda x : x > 5))
Out[21]: {False: [0, 1, 2, 3, 4, 5], True: [6, 7, 8, 9]}

撰写回答