为什么Python 3对exec的更改导致此代码失效?

11 投票
2 回答
2929 浏览
提问于 2025-04-16 20:44

我在StackOverflow上看了很多关于“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文档,还看了“Python 3.0的新特性”页面,但没有找到代码出错的原因。

2 个回答

6

为了补充上面的回答,以防万一。如果exec是在某个函数里,我建议使用三参数的版本,像这样:

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

这是最干净的解决方案,因为它不会影响到你周围的任何命名空间。相反,myfunc被存储在一个明确的字典d里。

11

你可以通过以下方式查看每个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会查找一个名为a_func的全局变量(LOAD_GLOBAL),而Python 2.7则会先在本地范围内查找(LOAD_NAME),如果找不到再去全局查找。

如果你在exec之后执行print(locals()),你会发现a_func是在__init__函数内部创建的。

我其实不太明白为什么要这样做,但这似乎是处理符号表的一种变化。

顺便说一下,如果你想在__init__方法的顶部创建一个a_func = None,让解释器知道这是一个局部变量,这样做是行不通的,因为字节码现在会变成LOAD_FAST,它不会去查找,而是直接从一个列表中获取值。

我看到的唯一解决办法是把globals()作为exec的第二个参数,这样就会把a_func创建为一个全局函数,可以通过LOAD_GLOBAL指令访问。

编辑

如果你去掉exec语句,Python 2.7会把字节码从LOAD_NAME改为LOAD_GLOBAL。所以,在Python 2.x中使用exec,你的代码会变得更慢,因为它需要在本地范围内查找变化。

而Python 3中的exec不是一个关键字,解释器无法确定它是否真的在执行新代码,还是在做其他事情……所以字节码不会改变。

例如:

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

总结

如果你不在乎结果被添加到全局命名空间,可以用exec('...', globals())来解决这个问题。

撰写回答