Python `exec` 命令在函数内执行无效的解决办法

Python 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 <cell line: 4>()
      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() 中包含了 a3 的映射,但在函数内部变量名a仍无法解析。

def func():
    exec('a=3')
    print(locals())
    print(a)
func()
{'a': 3}



---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

Input In [2], in <cell line: 1>()
----> 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()aid之后,可以清楚的发现:

  • a=2exec(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 <cell line: 6>()
      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 <string>:1, in <module>


NameError: name 'a' is not defined

打印一下locals()globals()就会发现,ab是局部变量,不存在于全局变量表中:

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__': <module 'builtins' (built-in)>, '__builtins__': <module 'builtins' (built-in)>, '_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': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x0000019B3F809370>>, 'exit': <IPython.core.autocall.ZMQExitAutocall object at 0x0000019B3F809A90>, 'quit': <IPython.core.autocall.ZMQExitAutocall object at 0x0000019B3F809A90>, '_': '', '__': '', '___': '', '_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': <function func1 at 0x0000019B3F8673A0>}

这种方式直接修改了全局变量值,可能导致全局变量被污染,因此并不推荐使用。

折中版:将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 <cell line: 5>()
      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

参考资料:

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值