itertools.chain.from_iterable 的奇怪行为

Weird behavior of itertools.chain.from_iterable

考虑这个代码片段:

>>> from itertools import chain
>>> foo = [0]
>>> for i in (1, 2):
...     bar = (range(a, a+i) for a in foo)
...     foo = chain(*list(bar))
...
>>> list(foo)
[0, 1]

这是有道理的——在循环的第一次迭代中,bar 等同于 iter([[0]]),而 foo 的计算结果为 chain([0]),这等同于 iter([0])。然后,在循环的第二次迭代中,bar 现在等同于 iter([[0, 1]]),foo 变为 iter([0, 1])。这就是为什么 list(foo)[0, 1].

当我使用 foo = sum(list(bar), []) 而不是 chain(*list(bar)) 时,list(foo) 也得到相同的结果。

现在考虑这个代码片段:

>>> from itertools import chain
>>> foo = [0]
>>> for i in (1, 2):
...     bar = (range(a, a+i) for a in foo)
...     foo = chain.from_iterable(bar)
...
>>> list(foo)
[0, 1, 1, 2]

如您所见,唯一的区别是 foo = chain.from_iterable(bar) 行,它使用 itertools.chain.from_iterable 而不是 itertools.chain

在我看来,itertools.chain(*list(iterable)) 大致相当于 itertools.chain.from_iterable(iterable),但这里并非如此。那为什么最后的结果不一样呢?

不同的是,在chain(*list(bar))中bar会立即耗尽,而在chain.from_iterable(bar)中则不会。而在bar的定义中,使用了i,也就是late-binding:它不是在定义的时候,而是从名字i中获取i的值=14=] 在评估时。

IOW,当您使用 foo = chain.from_iterable(bar) 时,bar 尚未评估。当您随后调用 list(foo) 并且它 "calls" bar 时,定义中的 i 获取名称 i 当前的值指的是——也就是2。

所以如果我们手动更改 i,我们应该能够适当地更改结果:

>>> from itertools import chain
>>> foo = [0]
>>> for i in (1, 2):
...         bar = (range(a, a+i) for a in foo)
...         foo = chain.from_iterable(bar)
...     
>>> i = 10
>>> list(foo)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]

区别在于生成器的使用和 foo = chain.from_iterable(bar) 的延迟求值。如果您将此行更改为 foo = chain.from_iterable(list(bar)),这两个程序将是等效的,这会强制 bar 生成器的计算以具体值为基础 foo。

否则,正如所写,这两个程序在语义上是不同的,因为前者将 chain 应用于列表,而第二个将 chain 应用于生成器,在某些方面可以将其视为函数句柄,它推迟执行直到最后的 list(foo) 在循环结束后被调用。

[这个答案在Python3中测试过,其中range是一个生成器。它可能在 Python 2.x 中表现不同,其中范围 returns 整个列表...]