Python exec 命令在函数内执行无效

文章目录

  • 问题复现
  • 解决方案
    • 简易版:将 `exec` 的执行结果保存到 `globals()`
    • 进阶版:将 `exec` 的执行结果保存到 `locals()`
    • 终极版:将 `exec` 的执行结果保存到自定义字典
  • 原因分析
  • 函数编译对 `locals()` 的影响
  • 解决方案原理
  • 参考

本文记录了使用 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 赋值,本质上是因为 execlocals() 中添加了 a3 的映射

>>> 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() 中包含了 a3 的映射,但是变量 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 是等价的。

  1. exec(s)
  2. exec(s, globals())
  3. exec(s, globals(), locals())
  4. exec(s, globals(), globals())

exec 执行过程中产生的变量会被写入第三个参数,也就是 __locals 中。

解决方案简易版 使用的是方式 2 ,等价于方式 4 ,也就是直接将 exec 执行过程中产生的变量写入 globals()

解决方案进阶版 使用的是方式 1 ,等价于方式 3 ,也就是直接将 exec 执行过程中产生的变量写入 locals() 。由于没有修改 globals() ,所以 exec 执行过程中产生的变量仅在函数内部可见。但是如果发生变量名重复,则写入 locals() 的操作将失败。

解决方案终极版 方式 3 的变体,区别仅在于方式 3 传入的 __localslocals() ,而解决方案终极版传入的 __locals 是自定义字典。因为 python 官方不建议修改 locals() ,所以只需要使用一个普通字典传入 __locals 即可解决解决方案进阶版的问题。自定义字典甚至可以使用 locals() 初始化。

参考

exec() not working inside function python3.x

Can’t access variable created by altering locals() inside function

你可能感兴趣的:(debug记录,python)