列表生成器产生的输出与预期不符

20

我有一个列表和一个定义为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]

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


它对我显示错误。 - Avinash Raj
@AvinashRaj 先运行第二个方法会出现 NameError: global name 'a' is not defined - Himanshu Mishra
1
你的问题就在这里,变量a在第一个函数中被定义为(3,4)。因此,list()函数总是针对该变量进行操作。 - TheGeorgeous
两个都不能在Python3中工作。 - Padraic Cunningham
请返回翻译后的文本:http://python-history.blogspot.ie/2010/06/from-list-comprehensions-to-generator.html https://dev59.com/mW855IYBdhLWcg3wsWhq#4199355 - Padraic Cunningham
6个回答

14

这个问题已在Python 3中得到解决。当您使用列表推导式[i(0) + i(1) for a in alist]时,您将在其周围的作用域中定义a,该作用域对i是可访问的。在新会话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
列表推导式不是生成器:生成器表达式和列表推导式
引用块: 生成器表达式用圆括号“()”括起来,而列表推导式用方括号“[]”括起来。
在您的例子中,作为一个类的list()有它自己的变量范围,并且最多可以访问全局变量。当您使用它时,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类,在其自己的范围内尝试将其转换为列表后返回。对于这个生成器来说,它在lambda函数内部没有任何访问权限,变量a没有意义。

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

与生成器相比,列表推导式的行为也在此处提到:here:

  

列表推导式也将它们的循环变量“泄漏”到周围的作用域中。这也将在Python 3.0中发生改变,使得Python 3.0中列表推导式的语义定义等同于列表()。Python 2.4及以上版本应该如果列表推导式的循环变量与紧邻的环境中使用的变量具有相同的名称,则发出弃用警告。

在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

它是如何同时生成输出的? - Avinash Raj
1
通过先运行列表推导式,a 仍然绑定到 (3, 4) 元组。 - Martijn Pieters

5

你应该将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]

编辑:这个答案只是一个简单的解决方法,并不是对问题的真正回答。观察到的效果稍微复杂一些,可以查看我的其他答案


5

这里需要理解的重要事情是:

  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历史博客文章从列表推导到生成器表达式

2
请看我在另一个答案中提供的解决方法。但是再仔细思考一下,这个问题似乎更加复杂。我认为这里有几个问题:
  • 当你执行i = lambda x: a[x]时,变量a不是函数的参数,这被称为闭包。无论是lambda表达式还是普通函数定义都是如此。

  • Python显然采用了“延迟绑定”的方式,这意味着你封闭的变量的值只在调用函数的时候查找。这可能会导致各种意外结果

  • 在Python 2中,列表推导和生成器表达式之间存在差异,列表推导泄漏它们的循环变量,而生成器表达式中的循环变量不会泄漏(有关详细信息,请参见这个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

1

a在全局范围内,所以它应该会出错。

解决方法是:

i = lambda a, x: a[x]


1
在执行 [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]

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