77

如果我对列表推导做某事,它会写入一个局部变量:

i = 0
test = any([i == 2 for i in xrange(10)])
print i

这将打印“9”。但是,如果我使用生成器,它不会写入局部变量:

i = 0
test = any(i == 2 for i in xrange(10))
print i

这将打印“0”。

这种差异有什么好的理由吗?这是一个设计决策,还是只是生成器和列表推导实现方式的随机副产品?就我个人而言,如果列表推导不写入局部变量,我觉得会更好。

4

6 回答 6

75

Python 的创建者 Guido van Rossum 在写关于统一构建到 Python 3 中的生成器表达式时提到了这一点:(强调我的)

我们还在 Python 3 中进行了另一项更改,以提高列表推导式和生成器表达式之间的等价性。在 Python 2 中,列表推导将循环控制变量“泄漏”到周围的作用域中:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

这是列表推导的原始实现的产物;多年来,它一直是 Python 的“肮脏小秘密”之一。它开始是为了让列表理解变得非常快而故意妥协,虽然这对初学者来说不是一个常见的陷阱,但它确实偶尔会刺痛人们。对于生成器表达式,我们不能这样做。生成器表达式是使用生成器实现的,其执行需要单独的执行框架。因此,生成器表达式(特别是如果它们在短序列上迭代时)不如列表推导效率高。

然而,在 Python 3 中,我们决定通过使用与生成器表达式相同的实现策略来修复列表推导的“肮脏小秘密”。因此,在 Python 3 中,上面的示例(修改为使用 print(x) :-) 将打印 'before',证明列表推导中的 'x' 暂时隐藏但不会覆盖周围的 'x'范围。

所以在 Python 3 中你不会再看到这种情况发生了。

有趣的是, Python 2 中的dict 推导式也不这样做。这主要是因为 dict 理解是从 Python 3 向后移植的,因此已经在其中进行了修复。

还有一些其他问题也涵盖了该主题,但我相信您在搜索该主题时已经看到了这些问题,对吧?;)

于 2013-11-07T22:38:45.980 回答
16

正如PEP 289(生成器表达式)所解释的:

循环变量(如果它是简单变量或简单变量的元组)不会暴露给周围的函数。这有助于实施并使典型用例更加可靠。

它似乎是出于实施原因而完成的。

就我个人而言,如果列表推导不写入局部变量,我觉得会更好。

PEP 289 也澄清了这一点:

列表推导还将它们的循环变量“泄漏”到周围的范围内。这在 Python 3.0 中也将发生变化,因此 Python 3.0 中列表推导式的语义定义将等同于 list()。

换句话说,您描述的行为在 Python 2 中确实有所不同,但在 Python 3 中已得到修复。

于 2013-11-07T22:35:28.993 回答
10

就我个人而言,如果列表推导不写入局部变量,我觉得会更好。

你是对的。这在 Python 3.x 中已修复。该行为在 2.x 中没有改变,因此它不会影响 (ab) 使用此漏洞的现有代码。

于 2013-11-07T22:35:11.410 回答
4

因为……因为……

不,真的,就是这样。实施的怪癖。并且可以说是一个错误,因为它已在 Python 3 中修复。

于 2013-11-07T22:35:12.300 回答
1

作为徘徊如何实际实施列表理解的副产品,我为您的问题找到了一个很好的答案。

在 Python 2 中,查看为简单列表理解生成的字节码:

>>> s = compile('[i for i in [1, 2, 3]]', '', 'exec')
>>> dis(s)
  1           0 BUILD_LIST               0
              3 LOAD_CONST               0 (1)
              6 LOAD_CONST               1 (2)
              9 LOAD_CONST               2 (3)
             12 BUILD_LIST               3
             15 GET_ITER            
        >>   16 FOR_ITER                12 (to 31)
             19 STORE_NAME               0 (i)
             22 LOAD_NAME                0 (i)
             25 LIST_APPEND              2
             28 JUMP_ABSOLUTE           16
        >>   31 POP_TOP             
             32 LOAD_CONST               3 (None)
             35 RETURN_VALUE  

它本质上转化为一个简单for-loop的 ,这就是它的语法糖。结果,与for-loopsapply 相同的语义:

a = []
for i in [1, 2, 3]
    a.append(i)
print(i) # 3 leaky

在列表理解的情况下,(C)Python 使用“隐藏列表名称”和特殊指令LIST_APPEND来处理创建,但实际上仅此而已。

因此,您的问题应该概括为为什么 Python 会写入for-loops 中的 for 循环变量;Eli Bendersky 的博客文章很好地回答了这个问题。

正如其他人所提到的那样,Python 3 已经改变了列表理解语义以更好地匹配生成器的语义(通过为理解创建单独的代码对象),并且本质上是以下内容的语法糖:

a = [i for i in [1, 2, 3]]

# equivalent to
def __f(it):
    _ = []
    for i in it
        _.append(i)
    return _
a = __f([1, 2, 3])

这不会泄漏,因为它不像 Python 2 等效项那样在最高范围内运行。i被泄漏,仅在该__f函数中作为局部变量被销毁。

如果您愿意,请查看通过运行为 Python 3 生成的字节码dis('a = [i for i in [1, 2, 3]]')。您将看到如何加载“隐藏”代码对象,然后最后进行函数调用。

于 2017-02-26T02:26:47.900 回答
0

上面 poke 描述的肮脏秘密的微妙后果之一是,list(...)[...]在 Python 2 中没有相同的副作用:

In [1]: a = 'Before'
In [2]: list(a for a in range(5))
In [3]: a
Out[3]: 'Before'

因此列表构造函数中的生成器表达式没有副作用,但副作用存在于直接列表理解中:

In [4]: [a for a in range(5)]
In [5]: a
Out[5]: 4
于 2016-10-28T10:51:37.130 回答