本文记录了使用 exec
命令可能导致的 bug ,并且提供了两种解决方案。这个 bug 的产生和 python 解析变量名的过程有关,详细的原因分析和解决思路可以在后文中看到。尽管 exec
的使用会导致许多工程上的问题,但是这并不意味着他不应该被使用,而是应该在必要时被小心地使用。无论是否使用 exec
,了解这个潜在的 bug 都将对理解 python 的运行过程有益。 本文对 locals()
进行了许多赋值操作,只是为了探究 exec
的执行效果。 python 官方文档 不建议这样操作。
注意:要复现本文的结果,必须使用 python REPL ,并且在执行每段代码前重启 python 解释器。从文件执行,输出信息可能有一定区别,不过没有本质区别。
直观上,在函数内部执行 exec('a = 3')
应该等价于 a = 3
,但是实际上变量 a
并没有被定义。本文的目标是在执行 exec('a = 3')
后,在 func
内部正常访问变量 a
。
>>> def func():
... exec('a = 3')
... print(a)
...
>>> func()
Traceback (most recent call last):
File "" , line 1, in <module>
File "" , line 3, in func
NameError: name 'a' is not defined
另一个与之相关的问题是,当函数局部变量 a
已经被定义时,在函数内部执行 exec('a = 3')
无法修改其值。
>>> def func():
... a = 2
... exec('a = 3')
... print(a)
...
>>> func()
2
exec
的执行结果保存到 globals()
这种方案容易实现,但是可能导致全局变量被污染。
>>> def func1():
... exec('a = 3', globals())
... print(a)
...
>>> func1()
3
>>> print(a)
3
该方案无法实现函数局部变量的修改
>>> def func1():
... a = 2
... exec('a = 3', globals())
... print(a)
...
>>> func1()
2
>>> print(a)
3
exec
的执行结果保存到 locals()
这种方案将 exec
的作用域限制在函数内部,但是使用时有一定限制:执行结果的变量名和函数内的变量名不能重复。
>>> def func2():
... exec('a = 3')
... b = locals()['a']
... print(b)
...
>>> func2()
3
如果违反了上述限制,这一方案将失效
>>> def func2():
... exec('a = 3')
... a = locals()['a']
... print(a)
...
>>> func2()
Traceback (most recent call last):
File "" , line 1, in <module>
File "" , line 3, in func2
KeyError: 'a'
同时,该方案无法实现函数局部变量的修改
>>> def func2():
... a = 2
... exec('a = 3')
... b = locals()['a']
... print(a, b)
...
>>> func2()
2 2
exec
的执行结果保存到自定义字典这种方法解除了上一方法中对变量名的限制,但是代码修改量稍大。
>>> def func3():
... d = {}
... exec('a = 3', globals(), d)
... a = d['a']
... print(a)
...
>>> func3()
3
同时,该方案可以实现函数局部变量的修改
>>> def func3():
... a = 2
... d = locals()
... exec('a = 3', globals(), d)
... a = d['a']
... print(a)
...
>>> func3()
3
REPL 中执行 exec
可以对 a
赋值,本质上是因为 exec
向 locals()
中添加了 a
到 3
的映射
>>> print(locals())
{...}
>>> exec('a = 3')
>>> print(locals())
{..., 'a': 3}
>>> print(a)
3
这一过程和普通赋值是一样的
>>> print(locals())
{...}
>>> a = 3
>>> print(locals())
{..., 'a': 3}
>>> print(a)
3
我们甚至可以直接修改 locals()
来实现赋值
>>> print(locals())
{...}
>>> locals()['a'] = 3
>>> print(locals())
{..., 'a': 3}
>>> print(a)
3
但是在函数内部,我们无法通过修改 locals()
来实现赋值
>>> def func():
... print(locals())
... locals()['a'] = 3
... print(locals())
... print(a)
...
>>> func()
{}
{'a': 3}
Traceback (most recent call last):
File "" , line 1, in <module>
File "" , line 5, in func
NameError: name 'a' is not defined
可以看出, locals()
中包含了 a
到 3
的映射,但是变量 a
还是无法解析。这就是造成 exec
执行无效的关键。
为什么会出现这个现象? 这是因为 func
在执行 print(a)
的时候,不是在 locals()
中查找 a
对应的值,而是在变量表中查找 a
对应的值。 locals()
只是变量表的一个拷贝,所以修改 locals()
不代表变量 a
真的被注册进了变量表。 python 函数的变量表是在编译时确定的,运行时无法修改。
为什么 REPL 中不会出现这个问题? 这是因为在 REPL 中, locals()
不是变量表的拷贝,而是 globals()
的引用。所以修改 locals()
本质上是在修改 globals()
。
>>> print(id(locals()))
4340004672
>>> print(id(globals()))
4340004672
>>> def func():
... print(id(locals()))
... print(id(globals()))
...
>>> func()
4340316736
4340004672
事实上,即使在函数内部,我们也可以通过修改 globals()
来实现赋值
>>> def func():
... print(globals())
... globals()['a'] = 3
... print(globals())
... print(a)
...
>>> print(globals())
{...}
>>> func()
{...}
{..., 'a': 3}
3
>>> print(globals())
{..., 'a': 3}
>>> print(a)
3
这也是解决方案简易版的思路。
locals()
的影响正常来说, python 的代码是按行执行的,所以后面的代码不应该影响前面的代码。比较下面两个函数
>>> def func():
... locals()['a'] = 2
... print(locals())
...
>>> def func1():
... locals()['a'] = 2
... print(locals())
... a = 3
...
>>> func()
{'a': 2}
>>> func1()
{}
可以看到, locals()['a']
是否赋值成功与 a = 3
是否出现在函数体中有关。 func1
中, a
是一个局部变量,因此在函数编译时预留了空间, locals()['a']
赋值失败。这就是为什么解决方案进阶版中,变量名不能重复的原因。
首先需要了解 exec
的参数。
(function) exec: (
__source: str | bytes | CodeType,
__globals: dict[str, Any] | None = ...,
__locals: Mapping[str, object] | None = ...,
/,
) -> None
exec
有三个参数(最后的 /
表示前面的参数只能以位置参数的形式传入,而不能以关键词参数的形式传入)
__source
是要执行的字符串__globals
是被执行字符串的全局变量( dict 类型),默认为 globals()
__locals
是被执行字符串的局部变量( mapping 类型),默认为 locals()
exec
的文档中明确指出,当 __globals
参数给定,则 __locals
参数的默认值就是 _globals
。
The source may be a string representing one or more Python statements or a code object as returned by compile(). The globals must be a dictionary and locals can be any mapping, defaulting to the current globals and locals. If only globals is given, locals defaults to it.
也就是说,在下面四种调用方式中, 1 和 3 是等价的, 2 和 4 是等价的。
exec(s)
exec(s, globals())
exec(s, globals(), locals())
exec(s, globals(), globals())
exec
执行过程中产生的变量会被写入第三个参数,也就是 __locals
中。
解决方案简易版 使用的是方式 2 ,等价于方式 4 ,也就是直接将 exec
执行过程中产生的变量写入 globals()
。
解决方案进阶版 使用的是方式 1 ,等价于方式 3 ,也就是直接将 exec
执行过程中产生的变量写入 locals()
。由于没有修改 globals()
,所以 exec
执行过程中产生的变量仅在函数内部可见。但是如果发生变量名重复,则写入 locals()
的操作将失败。
解决方案终极版 方式 3 的变体,区别仅在于方式 3 传入的 __locals
是 locals()
,而解决方案终极版传入的 __locals
是自定义字典。因为 python 官方不建议修改 locals()
,所以只需要使用一个普通字典传入 __locals
即可解决解决方案进阶版的问题。自定义字典甚至可以使用 locals()
初始化。
exec() not working inside function python3.x
Can’t access variable created by altering locals() inside function