问题是因为列表推导在exec()
.
当您在 之外创建一个函数(在本例中为列表理解)时exec()
,解析器会使用自由变量(代码块使用但未由它定义的变量,即g
在您的情况下)构建一个元组。这个元组被称为函数的闭包。它保存在__closure__
函数的成员中。
在 中时exec()
,解析器不会在列表推导上构建闭包,而是默认尝试查找globals()
字典。这就是为什么global g
在代码开头添加会起作用(以及globals().update(locals())
)。
在它的两个参数版本中使用exec()
也可以解决问题:Python 会将 globals() 和 locals() 字典合并到一个字典中(根据文档)。执行分配时,它同时在全局和局部中完成。由于 Python 将签入全局变量,因此这种方法将起作用。
这是对这个问题的另一种看法:
import dis
code = """
g = 5
x = [g for i in range(5)]
"""
a = compile(code, '<test_module>', 'exec')
dis.dis(a)
print("###")
dis.dis(a.co_consts[1])
此代码生成此字节码:
2 0 LOAD_CONST 0 (5)
3 STORE_NAME 0 (g)
3 6 LOAD_CONST 1 (<code object <listcomp> at 0x7fb1b22ceb70, file "<boum>", line 3>)
9 LOAD_CONST 2 ('<listcomp>')
12 MAKE_FUNCTION 0
15 LOAD_NAME 1 (range)
18 LOAD_CONST 0 (5)
21 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
24 GET_ITER
25 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
28 STORE_NAME 2 (x)
31 LOAD_CONST 3 (None)
34 RETURN_VALUE
###
3 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (i)
12 LOAD_GLOBAL 0 (g) <---- THIS LINE
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
注意它最后是如何执行LOAD_GLOBAL
加载g
的。
现在,如果您有此代码:
def Foo():
a = compile(code, '<boum>', 'exec')
dis.dis(a)
print("###")
dis.dis(a.co_consts[1])
exec(code)
Foo()
这将提供完全相同的字节码,这是有问题的:因为我们在一个函数中,g
不会在全局变量中声明,而是在函数的局部变量中声明。但是 Python 尝试在全局变量中搜索它(使用LOAD_GLOBAL
)!
这是解释器在 之外所做的exec()
:
def Bar():
g = 5
x = [g for i in range(5)]
dis.dis(Bar)
print("###")
dis.dis(Bar.__code__.co_consts[2])
这段代码给了我们这个字节码:
30 0 LOAD_CONST 1 (5)
3 STORE_DEREF 0 (g)
31 6 LOAD_CLOSURE 0 (g)
9 BUILD_TUPLE 1
12 LOAD_CONST 2 (<code object <listcomp> at 0x7fb1b22ae030, file "test.py", line 31>)
15 LOAD_CONST 3 ('Bar.<locals>.<listcomp>')
18 MAKE_CLOSURE 0
21 LOAD_GLOBAL 0 (range)
24 LOAD_CONST 1 (5)
27 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
30 GET_ITER
31 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
34 STORE_FAST 0 (x)
37 LOAD_CONST 0 (None)
40 RETURN_VALUE
###
31 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (i)
12 LOAD_DEREF 0 (g) <---- THIS LINE
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
如您所见,g
使用 加载LOAD_DEREF
,在 中生成的元组中可用,使用BUILD_TUPLE
加载变量。该语句创建了一个函数,就像前面看到的一样,但带有一个闭包。g
LOAD_CLOSURE
MAKE_CLOSURE
MAKE_FUNCTION
这是我对这种方式原因的猜测:第一次读取模块时,需要时创建闭包。执行时exec()
,无法实现其执行代码中定义的功能需要闭包。对他来说,其字符串中不以缩进开头的代码在全局范围内。知道他是否以需要闭包的方式调用的唯一方法是exec()
检查当前范围(这对我来说似乎很骇人听闻)。
这确实是一种晦涩难懂的行为,可以解释,但当它发生时肯定会引起一些人的注意。这是Python 指南中很好解释的副作用,尽管很难理解为什么它适用于这种特殊情况。
我所有的分析都是在 Python 3 上进行的,我没有在 Python 2 上尝试过任何东西。