列表(生成器)的意外输出

Unexpected output from list(generator)

我有一个列表和一个 lambda 函数定义为

In [1]: i = lambda x: a[x]
In [2]: alist = [(1, 2), (3, 4)]

然后我尝试了两种不同的方法来计算一个简单的总和

第一种方法。

In [3]: [i(0) + i(1) for a in alist]
Out[3]: [3, 7]

第二种方法。

In [4]: list(i(0) + i(1) for a in alist)
Out[4]: [7, 7]

两个结果出乎意料的不同。为什么会这样?

此行为已在 python 中得到修复 3. 当您使用列表理解 [i(0) + i(1) for a in alist] 时,您将在其周围范围内定义 ai 可以访问该范围.在新会话中 list(i(0) + i(1) for a in alist) 会抛出错误。

>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
>>> list(i(0) + i(1) for a in alist)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <genexpr>
  File "<stdin>", line 1, in <lambda>
NameError: global name 'a' is not defined

列表理解不是生成器:Generator expressions and list comprehensions

Generator expressions are surrounded by parentheses (“()”) and list comprehensions are surrounded by square brackets (“[]”).

在你的例子中 list() 作为一个 class 有它自己的变量范围并且它最多可以访问全局变量。当您使用它时,i 将在该范围内查找 a。在新会话中试试这个:

>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
>>> [i(0) + i(1) for a in alist]
[3, 7]
>>> a
(3, 4)

在另一个会话中将其与此进行比较:

>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
>>> l = (i(0) + i(1) for a in alist)
<generator object <genexpr> at 0x10e60db90>
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> [x for x in l]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <genexpr>
  File "<stdin>", line 1, in <lambda>
NameError: global name 'a' is not defined

当您 运行 list(i(0) + i(1) for a in alist) 时,您会将生成器 (i(0) + i(1) for a in alist) 传递给 list class,它将尝试将其转换为列表在 return 列表之前它自己的范围。对于这个无法访问 lambda 函数内部的生成器,变量 a 没有任何意义。

生成器对象 <generator object <genexpr> at 0x10e60db90> 丢失了变量名 a。然后当 list 尝试调用生成器时,lambda 函数将针对未定义的 a 抛出错误。

列表理解与生成器对比的行为也提到了here:

List comprehensions also "leak" their loop variable into the surrounding scope. This will also change in Python 3.0, so that the semantic definition of a list comprehension in Python 3.0 will be equivalent to list(). Python 2.4 and beyond should issue a deprecation warning if a list comprehension's loop variable has the same name as a variable used in the immediately surrounding scope.

在python3:

>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
>>> [i(0) + i(1) for a in alist]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <listcomp>
  File "<stdin>", line 1, in <lambda>
NameError: name 'a' is not defined

a 在全局范围内。 所以它应该给出错误

解决方案是:

i = lambda a, x: a[x]

您应该将 a 作为您的 lambda 函数的参数。这按预期工作:

In [10]: alist = [(1, 2), (3, 4)]

In [11]: i = lambda a, x: a[x]

In [12]: [i(a, 0) + i(a, 1) for a in alist]
Out[12]: [3, 7]

In [13]: list(i(a, 0) + i(a, 1) for a in alist)
Out[13]: [3, 7]

获得相同结果的另一种方法是:

In [14]: [sum(a) for a in alist]
Out[14]: [3, 7]

EDIT 这个答案只是一个简单的解决方法,并不是问题的真正答案。观察到的效果有点复杂,看我的.

执行[i(0) + i(1) for a in alist]后,a变为(3,4)

然后当下面一行被执行时:

list(i(0) + i(1) for a in alist)

(3,4) 值被 lambda 函数 i 两次用作 a 的值,因此它打印 [7,7].

相反,您应该定义具有两个参数 ax 的 lambda 函数。

i = lambda a,x : a[x]

这里需要了解的重要事项是

  1. 生成器表达式将在内部创建函数对象,但列表理解不会。

  2. 它们都将循环变量绑定到值,如果循环变量尚未创建,则它们将在当前范围内。

让我们看看生成器表达式的字节码

>>> dis(compile('(i(0) + i(1) for a in alist)', 'string', 'exec'))
  1           0 LOAD_CONST               0 (<code object <genexpr> at ...>)
              3 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (alist)
              9 GET_ITER            
             10 CALL_FUNCTION            1
             13 POP_TOP             
             14 LOAD_CONST               1 (None)
             17 RETURN_VALUE        

它加载代码对象,然后使它成为一个函数。让我们看看实际的代码对象。

>>> dis(compile('(i(0) + i(1) for a in alist)', 'string', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                27 (to 33)
              6 STORE_FAST               1 (a)
              9 LOAD_GLOBAL              0 (i)
             12 LOAD_CONST               0 (0)
             15 CALL_FUNCTION            1
             18 LOAD_GLOBAL              0 (i)
             21 LOAD_CONST               1 (1)
             24 CALL_FUNCTION            1
             27 BINARY_ADD          
             28 YIELD_VALUE         
             29 POP_TOP             
             30 JUMP_ABSOLUTE            3
        >>   33 LOAD_CONST               2 (None)
             36 RETURN_VALUE        

正如您在此处看到的,迭代器的当前值存储在变量 a 中。但是由于我们将其设为函数对象,因此创建的 a 将仅在生成器表达式中可见。

但是在列表理解的情况下,

>>> dis(compile('[i(0) + i(1) for a in alist]', 'string', 'exec'))
  1           0 BUILD_LIST               0
              3 LOAD_NAME                0 (alist)
              6 GET_ITER            
        >>    7 FOR_ITER                28 (to 38)
             10 STORE_NAME               1 (a)
             13 LOAD_NAME                2 (i)
             16 LOAD_CONST               0 (0)
             19 CALL_FUNCTION            1
             22 LOAD_NAME                2 (i)
             25 LOAD_CONST               1 (1)
             28 CALL_FUNCTION            1
             31 BINARY_ADD          
             32 LIST_APPEND              2
             35 JUMP_ABSOLUTE            7
        >>   38 POP_TOP             
             39 LOAD_CONST               2 (None)
             42 RETURN_VALUE        

没有明确的函数创建,变量a是在当前范围内创建的。因此,a 泄漏到当前范围。


有了这个理解,让我们来解决你的问题。

>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]

现在,当您使用理解创建列表时,

>>> [i(0) + i(1) for a in alist]
[3, 7]
>>> a
(3, 4)

您可以看到 a 已泄漏到当前范围,并且它仍然绑定到迭代中的最后一个值。

因此,当您在列表理解后迭代生成器表达式时,lambda 函数使用泄漏的 a。这就是为什么你得到 [7, 7],因为 a 仍然绑定到 (3, 4)

但是,如果您首先迭代生成器表达式,那么 a 将绑定到 alist 的值,并且不会泄漏到当前范围,因为生成器表达式成为一个函数。因此,当 lambda 函数尝试访问 a 时,它无法在任何地方找到它。这就是它因错误而失败的原因。

注意: 在 Python 3.x 中无法观察到相同的行为,因为通过为列表理解创建函数也可以防止泄漏。您可能想在 Guido 自己撰写的 Python 博客的 post、From List Comprehensions to Generator Expressions 的历史中阅读更多相关信息。

请参阅我的其他答案以了解解决方法。但是仔细想想,问题似乎有点复杂。我认为这里有几个问题:

  • 当你做i = lambda x: a[x]时,变量a不是参数 对于函数,这称为 closure。这对于 lambda 表达式和普通函数定义都是相同的。

  • Python 显然是 'late binding',这意味着您关闭的变量的值只会在您调用函数时查找。这会导致 various unexpected results.

  • 在 Python 2 中,列表推导式会泄漏其循环变量,而生成器表达式不会泄漏循环变量(请参阅 this PEP 了解详情)。此差异已在 Python 3 中删除,其中列表理解是 list(generater_expression) 的快捷方式。我不确定,但这可能意味着 Python2 列表理解在它们的外部范围内执行,而生成器表达式和 Python3 列表理解创建它们自己的内部范围。

演示(在 Python2 中):

In [1]: def f():  # closes over a from global scope
   ...:     return 2 * a
   ...: 

In [2]: list(f() for a in range(5))  # does not find a in global scope
[...]
NameError: global name 'a' is not defined

In [3]: [f() for a in range(5)]  
# executes in global scope, so f finds a. Also leaks a=8
Out[3]: [0, 2, 4, 6, 8]

In [4]: list(f() for a in range(5))  # finds a=8 in global scope
Out[4]: [8, 8, 8, 8, 8]

在Python3:

In [1]: def f():
   ...:     return 2 * a
   ...: 

In [2]: list(f() for a in range(5))  
# does not find a in global scope, does not leak a
[...]    
NameError: name 'a' is not defined

In [3]: [f() for a in range(5)]  
# does not find a in global scope, does not leak a
[...]
NameError: name 'a' is not defined

In [4]: list(f() for a in range(5))  # a still undefined
[...]
NameError: name 'a' is not defined