为什么列表推导写入循环变量,但生成器不写?

时间:2013-11-07 22:32:57

标签: python python-2.7 generator list-comprehension

如果我对列表推导做了些什么,它会写入一个局部变量:

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”。

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

6 个答案:

答案 0 :(得分:74)

Python的创建者Guido van Rossum在撰写关于{3}的内容时提到了这一点,这些内容统一构建在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 comprehensions 也不会这样做;这主要是因为dict理解是从Python 3向后移植的,因此已经有了修复它们。

还有一些其他问题也涵盖了这个主题,但我确定你在搜索主题时已经看过那些,对吧? ;)

答案 1 :(得分:16)

PEP 289(生成器表达式)解释:

  

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

这似乎是出于实施原因。

  

就个人而言,如果列表推导没有写入局部变量,那对我来说似乎更好。

PEP 289也澄清了这一点:

  

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

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

答案 2 :(得分:9)

  

就个人而言,如果列表推导没有写入局部变量,那对我来说似乎更好。

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

答案 3 :(得分:4)

因为......因为。

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

答案 4 :(得分: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-loops相同的语义适用:

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

在列表理解的情况下,(C)Python使用"隐藏列表名称"和一个特殊的指令LIST_APPEND来处理创作,但实际上除此之外什么也没做。

所以你的问题应该概括为什么Python写入for-loop s中的for循环变量;很好地回答by a blog post from 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]]')。你会看到"隐藏"加载代码对象,然后最后进行函数调用。

答案 5 :(得分:0)

上面提到的肮脏秘密的一个微妙后果是list(...)[...]在Python 2中没有相同的副作用:

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

因此,list-constructor中的生成器表达式没有副作用,但副作用存在于直接列表理解中:

In [4]: [a for a in range(5)]
In [5]: a
Out[5]: 4