exec
命令在函数内执行无效的解决办法我们都知道
exec
函数可以用来动态执行 python 代码,但如果在函数内执行会遇到问题,本文记录了具体问题、原因分析以及解决方案。
比如,如下执行命令exec('a=3')
,等同于a=3
:
exec('a=3')
print(a)
3
但如果把上述exec
命令封装于一个函数内部,则会报变量未被定义的错误。
def func():
exec('a=3')
print(a)
func()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Input In [3], in ()
2 exec('a=3')
3 print(a)
----> 4 func()
Input In [3], in func()
1 def func():
2 exec('a=3')
----> 3 print(a)
NameError: name 'a' is not defined
|
另一个与之相关的问题是,如果在函数内部通过exec
命令修改局部变量值,也会发现无法进行修改,比如下方示例:
def func():
a = 2
exec('a=3')
print(a)
func()
2
若想理解和解决上述遇到的问题,需了解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
。
理解了上述参数设置之后,要清楚若无特殊指定,
exec
执行过程中产生的变量会被写入第三个参数,也就是__locals
中。`
locals()
函数会以字典类型返回当前位置的全部局部变量。比如,在下方的示例中,可以看出,执行exec
命令后,locals()
中包含了 a
到 3
的映射,但在函数内部变量名a
仍无法解析。
def func():
exec('a=3')
print(locals())
print(a)
func()
{'a': 3}
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Input In [2], in ()
----> 1 func()
Input In [1], in func()
2 exec('a=3')
3 print(locals())
----> 4 print(a)
NameError: name 'a' is not defined
|
产生上述结果的原因在于:
首先,exec()
函数会引入一个新的作用域,其内部的变量名(比如a
)如果第一次出现,且出现在=
前面,即被视为定义了一个局部变量,其作用域仅在exec()
函数内部,这就是为什么exec
内部执行print(a)
时a
可以被成功解析的原因;
其次,exec()
函数的执行结果默认会被写入locals()
, 可以看到在exec
内部调用的locals()
,和在func
内调用的locals()
是同一个id
;
也就是说,虽然在exec()
外部无法访问其内创建的变量,但执行的结果已经被以键值对的形式写入了locals()
, 可以通过locals()["a"]
在exec
外部对值进行提取。
def func():
exec('a=3;print(f\'exec local:{id(locals())}, {locals()}\'); print(a)')
print(f'func local:{id(locals())},{locals()}')
func()
exec local:2271288676160, {'a': 3}
3
func local:2271288676160,{'a': 3}
再看下面这个通过exec
修改局部变量值的示例,打印出locals()
和a
的id
之后,可以清楚的发现:
a=2
和exec(a=3)
创建的是两个不同id
的局部变量(具有不同的作用域),类似于分属于两个不同文件夹的同名文件,所以在函数末尾print(a)
实际访问的是exec
外创建的局部变量a
,故打印出的值是2
;
exec
的结果随默认会写入locals()
,但在此例中,因为func
中创建了局部变量a
,因此在函数编译时预留了空间,exec
执行过程中产生的a
的值无法写入,这也就是为什么,通过exec
命令无法实现局部变量值修改的原因。
def func():
a = 2
print(f'a created outside exec:{id(a)}')
print(f'func local before exec:{id(locals())},{locals()}')
exec('a=3;\
print(f\'exec local:{id(locals())}, {locals()}\'); \
print(f\'a created inside exec:{id(a)}\')')
print(f'func local after exec:{id(locals())},{locals()}')
print(a)
func()
a created outside exec:140733117701920
func local before exec:2822478558272,{'a': 2}
exec local:2822478558272, {'a': 3}
a created inside exec:140733117701952
func local after exec:2822478558272,{'a': 2}
2
如果把exec
内部的变量名称做一下修改呢?通过下方的例子,我们可以看到,因为不存在变量名的冲突,exec
内部创建的变量b
赋值结果也被成功写入了locals()
。
def func():
a = 2
print(f'a created outside exec:{id(a)}')
print(f'func local before exec:{id(locals())},{locals()}')
exec('b=3;\
print(f\'exec local:{id(locals())}, {locals()}\'); \
print(f\'b created inside exec:{id(b)}\')')
print(f'func local after exec:{id(locals())},{locals()}')
func()
a created outside exec:140733117701920
func local before exec:2131686966592,{'a': 2}
exec local:2131686966592, {'a': 2, 'b': 3}
b created inside exec:140733117701952
func local after exec:2131686966592,{'a': 2, 'b': 3}
总结一下:
exec()
会引入一个新的作用域,其内部创建的变量的作用空间仅在exec
内部,在其外包函数内无法访问;exec()
执行结果默认写入locals()
,在locals()
中无变量名称冲突时,执行结果会以键值对的形式成功写入。
exec
执行结果保存到globals()
第一种简单粗暴的解决方案,就是将exec
的执行结果直接写入到全局变量globals()
中,这样在函数内部可以访问变量a
:
def func1():
exec('a=3',globals())
print(a)
func1()
3
但是这种方式无法实现局部变量的修改,因为现在exec
内部创建的变量a
现在通过globals()
参数设置成了全局变量,而func1
内创建的变量a
仍是函数内的局部变量!根据作用域链的规则顺序,函数内执行print(a)
会优先访问局部作用域:
def func1():
a = 2
exec('a=3',globals())
print(a)
func1()
2
可以看到全局变量已经被修改成了3:
print(a)
3
类似的原因,如果exec
内部需要访问func1
中创建的其他变量,也是不行的!
def func1():
a = 1
b = 2
exec('c=a+b',globals())
print(c)
func1()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Input In [3], in ()
4 exec('c=a+b',globals())
5 print(c)
----> 6 func1()
Input In [3], in func1()
2 a = 1
3 b = 2
----> 4 exec('c=a+b',globals())
5 print(c)
File :1, in
NameError: name 'a' is not defined
|
打印一下locals()
和globals()
就会发现,a
和b
是局部变量,不存在于全局变量表中:
def func1():
a = 1
b = 2
print(f'local: {locals()}')
print(f'global: {globals()}')
#exec('c=a+b',globals())
#print(c)
func1()
local: {'a': 1, 'b': 2}
global: {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', '__package__': None, '__loader__': None, '__spec__': None, '__builtin__': , '__builtins__': , '_ih': ['', "def func1():\n a = 1\n b = 2\n print(f'local: {locals()}')\n print(f'global: {globals()}')\n #exec('c=a+b',globals()) \n #print(c)\nfunc1()"], '_oh': {}, '_dh': [WindowsPath('D:/CODE/spkg-yilan/notes')], 'In': ['', "def func1():\n a = 1\n b = 2\n print(f'local: {locals()}')\n print(f'global: {globals()}')\n #exec('c=a+b',globals()) \n #print(c)\nfunc1()"], 'Out': {}, 'get_ipython': >, 'exit': , 'quit': , '_': '', '__': '', '___': '', '_i': '', '_ii': '', '_iii': '', '_i1': "def func1():\n a = 1\n b = 2\n print(f'local: {locals()}')\n print(f'global: {globals()}')\n #exec('c=a+b',globals()) \n #print(c)\nfunc1()", 'func1': }
这种方式直接修改了全局变量值,可能导致全局变量被污染,因此并不推荐使用。
exec
的执行结果保存到locals()
这种方式利用了exec
的执行结果会被写入locals()
的特点,但需注意:执行结果的变量名和函数内的变量名不能重复:
def func2():
exec('a=3')
b = locals()['a']
print(b)
func2()
3
如果违反了上述限制,这一方案失败:
func2
中,a
是一个局部变量,函数在编译时为a
预留了空间,exec
内部对a
的赋值操作因为locals()
中存在键冲突,执行写过无法写入,因此在执行a = locals()['a']
时locals()
中是不存在a
的def func2():
exec('a=3')
a = locals()['a']
print(a)
func2()
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Input In [3], in ()
3 a = locals()['a']
4 print(a)
----> 5 func2()
Input In [3], in func2()
1 def func2():
2 exec('a=3')
----> 3 a = locals()['a']
4 print(a)
KeyError: 'a'
|
同样的,由于locals()
中键的冲突问题,这种方案无法实现对局部变量的修改:
def func2():
a = 2
exec('a=3')
b = locals()['a']
print((a,b))
func2()
(2, 2)
这种方式,exec
内部可以访问func2
中创建的其他变量,因为exec
的作用域都被限制在函数内部了:
def func2():
a = 1
b = 2
print(f'local before exec :{locals()}, {id(locals())}')
exec('c=a+b;print(f\'local in exec:{locals()}, {id(locals())}\')')
print(f'local after exec:{locals()}, {id(locals())}')
func2()
local before exec :{'a': 1, 'b': 2}, 1824110744384
local in exec:{'a': 1, 'b': 2, 'c': 3}, 1824110744384
local after exec:{'a': 1, 'b': 2, 'c': 3}, 1824110744384
exec
的执行结果保存到自定义字典def func3():
d = {}
exec('a=3', globals(), d)
a = d['a']
print(a)
func3()
3
该方案可以实现函数局部变量的修改:
def func3():
a = 2
d = {}
exec('a=3', globals(), d)
a = d['a']
print(a)
func3()
3
如果需要在exec
内访问函数内创建的其他变量,需要将这些变量也写入自定义字典后,才可以在exec
内访问,因为自定义字典d
的设置,是指定exec()
代码执行时的局部变量以及收集代码执行后的局部变量为d
:
def func4():
d = {'a':1, 'b':2}
exec('c=a+b', globals(), d)
print(d['c'])
func4()
3
参考资料: