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


我在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()

# 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()

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


6415
2017-07-03 06:34


起源

这似乎很有可能让任何必须在5年内维持它的代码哭泣。 - Amber
我认为这更像是一个包含的例子,而不是实际使用的代码。我听到 exec 和 eval 在语言中占有一席之地。 - Ehtesh Choudhury
@Amber也许我是个虐待狂? - Jedidiah Hurt


答案:


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

>>> from dis import dis

并且,对于每个口译员:

#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)
...

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

如果你这样做 print(locals()) 之后 exec,你会看到的 a_func 是在里面创建的 __init__ 功能。

我真的不知道为什么这样做,但似乎是如何改变 符号表 被处理。

BTW,如果想创建一个 a_func = None 在你的 __init__ 使解释器知道它是一个局部变量的方法,因为字节码现在将不起作用 LOAD_FAST 并且不进行搜索但直接从列表中获取值。

我看到的唯一解决方案是添加 globals() 作为第二个参数 exec,这将创造 a_func 作为一个全局函数,可以通过访问 LOAD_GLOBAL 操作码。

编辑

如果删除了 exec 声明,Python2.7更改字节码 LOAD_NAME 至 LOAD_GLOBAL。所以,使用 exec,你的代码在Python2.x上总是会变慢,因为它必须在本地范围内搜索更改。

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

例如。

>>> exec = len
>>> exec([1,2,3])
3

TL;博士

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


10
2017-07-03 07:43



有效。很好的答案;字节码内省真的很酷。不知道那个。感谢分享。 - Jedidiah Hurt
@Jedidiah Hurt添加了更多信息。 - JBernardo
如 exec 正在考虑当地的范围,并作为 locals() 存储它,人们也可以 exec("a_func()") 要么 locals()['a_func']()。 - MGwynne
所以基本的问题是在2.7 python中做一些hackery注意到这样的事情 exec 是内置的,但现在 exec是“只是另一个功能”,这样的hackery已被删除? - MGwynne
@MGwynne很疯狂,你不觉得吗?但是有道理...... - JBernardo


完成上面的答案,以防万一。如果 exec 在某些功能中,我建议使用三参数版本如下:

def f():
    d = {}
    exec("def myfunc(): ...", globals(), d)
    d["myfunc"]()

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


5
2017-11-29 01:22



我喜欢这个,对我来说更安全:) - chisaipete