Python exec 命令在函数内执行无效 您所在的位置:网站首页 pythondef语句报错 Python exec 命令在函数内执行无效

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

2024-06-19 16:25| 来源: 网络整理| 查看: 265

文章目录 问题复现解决方案简易版:将 `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 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 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 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



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有