【问题标题】:Unexpected output from list(generator)列表(生成器)的意外输出
【发布时间】:2023-03-24 03:34:01
【问题描述】:

我有一个列表和一个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 python-2.7 list-comprehension generator-expression


【解决方案1】:

此行为已在 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

生成器表达式被括号(“()”)和列表包围 推导式用方括号(“[]”)括起来。

在您的示例中,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 没有意义。

生成器对象&lt;generator object &lt;genexpr&gt; at 0x10e60db90&gt; 丢失了变量名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

【讨论】:

  • 它如何为两者产生输出?
  • @AvinashRaj:通过首先运行列表推导,a 仍然绑定到 (3, 4) 元组。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

    有关解决方法,请参阅我的其他答案。但是再想一想,问题似乎就复杂了一些。我认为这里有几个问题:

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

    • Python 显然会进行“后期绑定”,这意味着您关闭的变量的值仅在您调用函数时才被查找。这可能导致various 意外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
    

    【讨论】:

      【解决方案4】:

      这里要了解的重要事项是

      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 历史博文From List Comprehensions to Generator Expressions 中阅读更多相关信息。

      【讨论】:

        【解决方案5】:

        [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]
        

        【讨论】:

          【解决方案6】:

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

          解决办法是:

          i = lambda a, x: a[x]

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-06-28
            • 2022-08-19
            • 1970-01-01
            • 1970-01-01
            • 2020-07-05
            • 2021-05-18
            • 1970-01-01
            • 2023-04-01
            相关资源
            最近更新 更多