为什么Python exec中的模块级变量无法访问?
我在一个项目中尝试使用Python的exec
来执行嵌入的Python代码。
我遇到的问题是,在exec
语句中创建的模块级变量在同一个模块中定义的函数里是无法访问的。
假设你有以下的Python程序:
x = 5
def foo():
print x
foo()
如果你把上面的四行代码放在一个文件里运行,应该是没有问题的。
但是,如果你尝试在exec
语句中运行这段代码,就会出问题。
下面是我们之前的程序,放在exec
语句里:
import __builtin__
global_env = {'__builtins__': __builtin__}
local_env = dict()
exec """
x = 5
def foo():
print x
foo()
""" in global_env, local_env
执行时,它不会正常工作,而是会出现以下错误:
Traceback (most recent call last):
File "lab.py", line 94, in <module>
""" in global_env, local_env
File "<string>", line 5, in <module>
File "<string>", line 4, in foo
NameError: global name 'x' is not defined
我原以为模块级变量是全局存储的,但看起来在exec
中并不是这样。
例如,在之前的例子中,如果你把对foo()
的调用替换成:
print global_env
print local_env
你会得到:
{'__builtins__': <module '__builtin__' (built-in)>}
{'x': 5, 'foo': <function foo at 0x102c12938>}
所以在模块级定义的任何东西(包括x
)都存储在locals()
中。
但是,除了在exec
语句的模块级别外,x
是无法从其他地方访问的。特别是,正如我们上面看到的,x
的局部作用域对在同一个exec
语句中定义的函数是不可见的。
解决方法
我找到两种方法来解决这个问题,让x
再次可访问。
第一种方法是在函数中使用global
关键字:
exec """
x = 5
def foo():
global x
print x
foo()
""" in global_env, local_env
第二种方法是在exec
中使用相同的字典作为globals()
和locals()
:
exec """
x = 5
def foo():
print x
foo()
""" in global_env
不过,这些只是半解决方案,并没有真正解决原来的问题。
所以我想问的是:为什么在exec
中模块级变量是局部存储的,为什么只能在模块级别访问?
一些相关的StackOverflow帖子:
4 个回答
Python的这个特性有个有趣的结果。如果你在一个模块级别的exec
语句中导入一个模块,那么在同一个模块中定义的函数也无法访问这个模块。换句话说,问题中稍微修改过的例子是行不通的,原因和答案中解释的一样:
exec """
import x
def foo():
print x
foo()
""" in global_env, local_env
问题中提供的两种解决方法可以用来修复这个问题。
你的问题假设了 x
是一个模块级别的变量,但实际上并不是。
在模块级别,globals()
和 locals()
是一样的(这可能是CPython特有的),而在函数内部,它们是不同的:
def foo():
print globals() is locals()
print globals() is locals()
foo()
当你使用 exec
语句并指定作用域时,它并不会在模块级别执行代码,而是按照你指定的作用域来执行。所以,你最后的代码片段并不是一个半解决方案或变通方法,它 准确地 解决了问题。
以下代码在模块级别出现时也能正常工作。它与你之前的代码意义不同,因为它是在模块中给 x
和 foo
赋值,所以它确实让 x
成为一个模块级别的变量:
exec '''
x = 5
def foo():
print x
foo()
'''
最后一段代码在函数内部不工作的原因,以及你第一次尝试不成功的原因,正如Blckknght的回答所说。使用不同作用域的 exec
执行时,表现得“就像在类定义中”,而不是“就像在模块中”或“就像在函数中”。这意味着你在里面定义的任何函数都无法访问它们周围的命名空间。它们只能访问自己的局部命名空间和 exec
语句的全局命名空间,而这两个地方都不是 x
被定义的地方。
你看到的行为是有详细说明的:
在所有情况下,如果省略了可选部分,代码会在当前的作用域中执行。如果只指定了“in”后面的第一个表达式,它应该是一个字典,这个字典会同时用于全局变量和局部变量。如果给出了两个表达式,它们分别用于全局变量和局部变量。如果提供了局部变量,局部变量可以是任何映射对象。记住,在模块级别,
globals
和locals
是同一个字典。 如果给出了两个不同的对象作为全局和局部变量,代码会像是在类定义中嵌入的一样执行。
实际上:
In [1]: class Test:
...: x = 5
...: def foo():
...: print(x)
...: foo()
...:
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-1-f20229bce3a1> in <module>()
----> 1 class Test:
2 x = 5
3 def foo():
4 print(x)
5 foo()
<ipython-input-1-f20229bce3a1> in Test()
3 def foo():
4 print(x)
----> 5 foo()
6
<ipython-input-1-f20229bce3a1> in foo()
2 x = 5
3 def foo():
----> 4 print(x)
5 foo()
6
NameError: name 'x' is not defined
你看到的行为正是预期的。如果你想让代码像在模块级别那样执行,你必须使用同一个对象作为全局和局部变量,所以你的解决方法是正确的。
要理解发生了什么,你需要仔细阅读这份文档。关键部分提到:
如果给定了两个不同的对象作为全局变量和局部变量,那么代码的执行就像是在一个类定义中嵌入的一样。
这意味着,局部变量会被放到局部命名空间里(相当于类级别的变量),但是如果函数(也就是方法)试图引用一个局部(类)变量,它就不会变成闭包。
把你的代码和下面的代码对比一下:
class Test(object):
x = 1
def foo():
print x
foo()
你会得到同样的错误,原因也是一样的。foo
不是一个闭包,所以它试图在全局命名空间中引用 x
(但没有成功)。