如果我对列表推导做了些什么,它会写入一个局部变量:
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”。
这种差异有什么好的理由吗?这是设计决策,还是生成器和列表推导的实现方式的随机副产品?就个人而言,如果列表推导没有写入局部变量,那对我来说似乎更好。
答案 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