列表推导式中的生成器表达式未按预期工作

3
以下代码产生了预期的输出:
# using a list comprehension as the first expression to a list comprehension
>>> l = [[i*2+x for x in j] for i,j in zip([0,1],[range(4),range(4)])]
>>> l[0]
[0, 1, 2, 3]
>>> l[1]
[2, 3, 4, 5]

然而,当我使用生成器表达式时,得到了不同的结果:
# using a generator expression as the first expression
>>> l = [(i*2+x for x in j) for i,j in zip([0,1],[range(4),range(4)])]
>>> list(l[0])
[2, 3, 4, 5]
>>> list(l[1])
[2, 3, 4, 5]
>>> list(l[0])
[]
>>> list(l[1])
[]
>>> l
[<generator object <listcomp>.<genexpr> at 0x7fddfa413ca8>, <generator object <listcomp>.<genexpr> at 0x7fddfa413c50>]

我知道生成器表达式只能使用一次,但是在这种情况下我为什么会得到两个相同的列表,特别是因为生成器对象看起来是唯一的,我很困惑。

我在Python 3.6.5上测试了这个问题,你能告诉我我错在哪里吗?

2个回答

4

i在每次生成器表达式执行时都与1绑定。生成器表达式不会捕获创建时生效的绑定 - 它们使用执行时生效的绑定。

>>> j = 100000
>>> e = (j for i in range(3))
>>> j = -6
>>> list(e)
[-6, -6, -6]

3
生成器对象是独一无二的,但它们是引用ij的,当列表推导终止时(这本质上创建了一个函数作用域,就像列表推导内部的生成器表达式一样)。因此,ij具有值i == 1j == range(4)。您甚至可以内省这个过程:
In [1]: l = [(i*2+x for x in j) for i,j in zip([0,1],[range(4),range(4)])]

In [2]: g = l[0]

In [3]: g.gi_frame.f_locals
Out[3]: {'.0': <range_iterator at 0x10e9be960>, 'i': 1}

这基本上是为什么经常出现令人惊讶的行为的原因:

In [4]: fs = [lambda: i for i in range(3)]

In [5]: fs[0]
Out[5]: <function __main__.<listcomp>.<lambda>()>

In [6]: fs[0]()
Out[6]: 2

In [7]: fs[1]()
Out[7]: 2

In [8]: fs[2]()
Out[8]: 2

您可以使用同样的解决方案来解决这个问题,即创建另一个封闭作用域,将变量本地绑定到不会改变的内容。使用函数(这里使用lambda,但也可以是普通函数)完美地解决了这个问题:
In [9]: l = [(lambda i, j: (i*2+x for x in j))(i, j) for i,j in zip([0,1],[range(4),range(4)])]

In [10]: list(l[0])
Out[10]: [0, 1, 2, 3]

In [11]: list(l[1])
Out[11]: [2, 3, 4, 5]

为了更加清晰,也许我会使用不同的参数名称来使情况更加明显:

In [12]: l = [(lambda a, b: (a*2+x for x in b))(i, j) for i,j in zip([0,1],[range(4),range(4)])]

In [13]: list(l[0])
Out[13]: [0, 1, 2, 3]

In [14]: list(l[1])
Out[14]: [2, 3, 4, 5]

这很有道理;我以为它会自动实现某种类型的闭包。我喜欢通过lambda强制执行新作用域的想法。 - Mike Lui
1
@MikeLui 它确实创建了一个闭包。本质上,生成器表达式使用函数作用域。这就是为什么列表推导中的lambda:i(它们创建闭包)始终返回2的原因。 - juanpa.arrivillaga

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接