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

Him*_*hra 20 python list-comprehension generator-expression python-2.7

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

In [1]: i = lambda x: a[x]
In [2]: alist = [(1, 2), (3, 4)]
Run Code Online (Sandbox Code Playgroud)

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

第一种方法.

In [3]: [i(0) + i(1) for a in alist]
Out[3]: [3, 7]
Run Code Online (Sandbox Code Playgroud)

第二种方法.

In [4]: list(i(0) + i(1) for a in alist)
Out[4]: [7, 7]
Run Code Online (Sandbox Code Playgroud)

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

Meh*_*hdi 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
Run Code Online (Sandbox Code Playgroud)

列表推导不是生成器:生成器表达式和列表推导.

生成器表达式用括号("()")括起来,列表推导用方括号("[]")括起来.

在您的示例中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)
Run Code Online (Sandbox Code Playgroud)

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

>>> 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
Run Code Online (Sandbox Code Playgroud)

当你运行时,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
Run Code Online (Sandbox Code Playgroud)


Bas*_*els 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]
Run Code Online (Sandbox Code Playgroud)

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

In [14]: [sum(a) for a in alist]
Out[14]: [3, 7]
Run Code Online (Sandbox Code Playgroud)

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


the*_*eye 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        
Run Code Online (Sandbox Code Playgroud)

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

>>> 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        
Run Code Online (Sandbox Code Playgroud)

如您所见,迭代器的当前值存储在变量中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        
Run Code Online (Sandbox Code Playgroud)

没有显式函数创建,并且a在当前范围中创建变量.因此,a泄露到当前范围.


有了这种理解,让我们解决您的问题.

>>> i = lambda x: a[x]
>>> alist = [(1, 2), (3, 4)]
Run Code Online (Sandbox Code Playgroud)

现在,当您创建具有理解力的列表时,

>>> [i(0) + i(1) for a in alist]
[3, 7]
>>> a
(3, 4)
Run Code Online (Sandbox Code Playgroud)

你可以看到a泄漏到当前范围,它仍然绑定到迭代的最后一个值.

因此,当您在列表推导之后迭代生成器表达式时,该lambda函数使用泄漏a.这就是为什么你会得到[7, 7],因为a仍然必然(3, 4).

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

注意:在Python 3.x中无法观察到相同的行为,因为通过为列表推导创建函数也可以防止泄漏.您可能希望在Guido自己撰写的Python历史博客文章" 从列表理解到生成器表达式"中阅读更多相关内容.