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]

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

4

6 回答 6

15

此行为已在 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()中,作为一个类具有自己的变量范围,并且最多可以访问全局变量。当您使用它时,ia在该范围内查找。在新会话中试试这个:

>>> 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 函数将抛出 undefined 错误a

与生成器相比,列表推导的行为也在这里提到:

列表推导还将它们的循环变量“泄漏”到周围的范围内。这在 Python 3.0 中也将发生变化,因此 Python 3.0 中列表推导式的语义定义将等同于 list()。如果列表推导的循环变量与紧邻范围内使用的变量同名,则 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
于 2015-07-04T09:03:31.617 回答
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]

编辑这个答案只是一个简单的解决方法,并不是问题的真正答案。观察到的效果有点复杂,请参阅我的其他答案

于 2015-07-04T09:05:28.203 回答
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。但由于我们将其设为函数对象,因此acreated 将仅在生成器表达式中可见。

但在列表理解的情况下,

>>> 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将绑定到 from 的值alist并且不会泄漏到当前范围。因此,当该lambda函数尝试访问a时,它无法在任何地方找到它。这就是它失败并出现错误的原因。

注意:在 Python 3.x 中无法观察到相同的行为,因为还可以通过为列表推导创建函数来防止泄漏。您可能想在 Python 的历史博客文章From List Comprehensions to Generator Expressions中阅读更多相关信息,该文章由 Guido 本人撰写。

于 2015-07-04T09:41:16.180 回答
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
于 2015-07-04T09:49:08.600 回答
1

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

解决方案是:

i = lambda a, x: a[x]

于 2015-07-04T09:05:02.737 回答
1

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

然后当执行以下行时:

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

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

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

i = lambda a,x : a[x]
于 2015-07-04T09:09:45.673 回答