文章目录
本文记录了使用 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 "<stdin>", line 1, in <module>
File "<stdin>", 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 "<stdin>", line 1, in <module>
File "<stdin>", 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 "<stdin>", line 1, in <module>
File "<stdin>", 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
本文探讨了在函数内部使用exec导致的变量访问问题,提供了解决方案,包括简易版(globals())、进阶版(locals())和终极版(自定义字典)。关键在于理解python的变量查找机制和函数编译时局部变量的存储。
1135

被折叠的 条评论
为什么被折叠?



