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 整个列表...]
考虑这个代码片段:
>>> 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 整个列表...]