为什么Python 3更改为exec会破坏此代码?

Jed*_*urt 11 python metaprogramming exec backwards-compatibility python-3.x

我在SO上查看了无数的"Python exec"主题,但找不到能解决我问题的主题.非常抱歉,如果之前有人问过这个问题.这是我的问题:

# Python 2.6: prints 'it is working'
# Python 3.1.2: "NameError: global name 'a_func' is not defined"
class Testing(object):
  def __init__(self):
    exec("""def a_func():
      print('it is working')""")
    a_func()

Testing()
Run Code Online (Sandbox Code Playgroud)
# Python 2.6: prints 'it is working'
# Python 3.1.2: prints 'it is working'
class Testing(object):
  def __init__(self):
    def a_func():
      print('it is working')
    a_func()

Testing()
Run Code Online (Sandbox Code Playgroud)

由于标准函数定义适用于两个Python版本,我假设问题必须是对exec工作方式的改变.我阅读了2.6和3的API文档,并exec阅读了"Python 3.0中的新功能"页面,并且看不出代码中断的任何原因.

JBe*_*rdo 10

您可以看到每个Python版本生成的字节码:

>>> from dis import dis
Run Code Online (Sandbox Code Playgroud)

并且,对于每个翻译:

#Python 3.2
>>> dis(Testing.__init__)
...
  5          10 LOAD_GLOBAL              1 (a_func)
...

#Python 2.7
>>> dis(Testing.__init__)
...
  5           8 LOAD_NAME                0 (a_func)
...
Run Code Online (Sandbox Code Playgroud)

如您所见,Python 3.2搜索名为的全局值(LOAD_GLOBAL)a_func,2.7在搜索全局范围之前首先搜索本地范围(LOAD_NAME).

如果你这样做print(locals())exec,你会看到a_func在内部创建__init__功能.

我真的不知道为什么会这样做,但似乎是对符号表处理方式的改变.

顺便说一句,如果想要a_func = None在你的__init__方法之上创建一个让解释器知道它是一个局部变量,它将无法工作,因为字节码现在将是LOAD_FAST并且不进行搜索但直接从列表中获取值.

我看到的唯一解决方案是添加globals()第二个参数exec,以便创建a_func一个全局函数,可以由LOAD_GLOBAL操作码访问.

编辑

如果删除该exec语句,Python2.7 会将字节码更改LOAD_NAMELOAD_GLOBAL.因此,使用exec,您的代码在Python2.x上总是会变慢,因为它必须在本地范围内搜索更改.

由于Python3 exec不是关键字,解释器无法确定它是否真正执行新代码或执行其他操作......因此字节码不会更改.

例如

>>> exec = len
>>> exec([1,2,3])
3
Run Code Online (Sandbox Code Playgroud)

TL;博士

exec('...', globals()) 如果你不关心将结果添加到全局命名空间,可以解决问题


Arm*_*igo 6

完成上面的答案,以防万一.如果exec是某个函数,我建议使用三参数版本,如下所示:

def f():
    d = {}
    exec("def myfunc(): ...", globals(), d)
    d["myfunc"]()
Run Code Online (Sandbox Code Playgroud)

这是最干净的解决方案,因为它不会修改您脚下的任何命名空间.相反,myfunc存储在显式字典中d.