Python 2 和 Python 3 中 exec 函数的行为

49 投票
4 回答
47021 浏览
提问于 2025-04-17 17:09

下面的代码在 Python2Python3 中输出结果不同:

from sys import version

print(version)

def execute(a, st):
    b = 42
    exec("b = {}\nprint('b:', b)".format(st))
    print(b)
a = 1.
execute(a, "1.E6*a")

Python2 输出:

2.7.2 (default, Jun 12 2011, 15:08:59) [MSC v.1500 32 bit (Intel)]
('b:', 1000000.0)
1000000.0

Python3 输出:

3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)]
b: 1000000.0
42

为什么 Python2execute 函数内部把变量 b 绑定到 exec 函数字符串中的值上,而 Python3 不这样做呢?我该如何在 Python3 中实现 Python2 的这种行为?我已经尝试过在 Python3 中给 exec 函数传递字典作为全局和局部变量,但到目前为止都没有成功。

--- 编辑 ---

在阅读了 Martijn 的回答后,我进一步用 Python3 分析了这个问题。在下面的例子中,我将 locals() 字典作为 d 传给 exec,但是 d['b'] 打印的内容和直接打印 b 的结果不一样。

from sys import version

print(version)

def execute(a, st):
    b = 42
    d = locals()
    exec("b = {}\nprint('b:', b)".format(st), globals(), d)
    print(b)                     # This prints 42
    print(d['b'])                # This prints 1000000.0
    print(id(d) == id(locals())) # This prints True
a = 1.
execute(a, "1.E6*a")

3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)]
b: 1000000.0
42
1000000.0
True

比较 dlocals() 的 ID 显示它们是同一个对象。但在这种情况下,b 应该和 d['b'] 是一样的。我的例子中出了什么问题呢?

4 个回答

4

总结一下:

  • Python 2 和 Python 3 都没有错误。
  • exec 的不同表现是因为在 Python 2 中它是一个语句,而在 Python 3 中它变成了一个函数。

请注意:

这里没有什么新东西。这只是把其他答案和评论中的真相整理在一起。我只是想把一些比较晦涩的细节讲清楚。

Python 2 和 Python 3 之间唯一的区别是,exec 在 Python 2 中可以改变外部函数的局部作用域(因为它是一个语句,可以访问当前的局部作用域),而在 Python 3 中就不能了(因为它现在是一个函数,所以在自己的局部作用域中运行)。

不过,这种困惑和 exec 语句没有关系,它只是源于一个特殊的行为细节:

locals() 返回的东西,我想称之为“一个作用域内可变的单例,它在调用 locals() 后始终只引用局部作用域中的所有变量”。

请注意,locals() 的行为在 Python 2 和 3 之间没有变化。因此,这种行为加上 exec 工作方式的变化看起来有些不稳定,但其实并不是,它只是暴露了一些一直存在的细节。

什么是“一个作用域内可变的单例,它引用局部作用域中的变量”?

  • 它是一个 作用域内单例,因为无论你在同一个作用域中调用 locals() 多次,返回的对象始终是相同的。
    • 因此可以观察到,id(d) == id(locals()),因为 dlocals() 引用的是同一个对象,同一个单例,因为在同一个作用域中只能有一个(在不同的作用域中你会得到不同的对象,但在同一个作用域中你只会看到这个单一的对象)。
  • 它是 可变的,因为它是一个普通对象,所以你可以改变它。
    • locals() 强制对象中的所有条目再次引用局部作用域中的变量。
    • 如果你通过 d 改变对象中的某些内容,这会改变对象,因为它是一个普通的可变对象。
  • 这些单例的变化不会反映回局部作用域,因为对象中的所有条目都是 对局部作用域中变量的引用。所以如果你改变条目,这些变化只会改变单例对象,而不会改变“引用之前指向的内容”(因此你不会改变局部变量)。

    • 在 Python 中,字符串和数字是不可变的。这意味着,如果你给一个条目赋值,你并不会改变条目指向的对象,而是引入一个新对象并将其引用赋给该条目。例如:

      a = 1
      d = locals()
      d['a'] = 300
      # d['a']==300
      locals()
      # d['a']==1
      

    除了优化外,这样做会:

    • 创建一个新对象 Number(1) - 这也是另一个单例。
    • 将指针存储到这个 Number(1) 中,放入 LOCALS['a']
      (其中 LOCALS 是内部局部作用域)
    • 如果还不存在,创建 SINGLETON 对象
    • 更新 SINGLETON,使其引用 LOCALS 中的所有条目
    • SINGLETON 的指针存储到 LOCALS['d']
    • 创建 Number(300),这不是单例。
    • 将指针存储到这个 Number(300) 中,放入 d['a']
    • 因此 SINGLETON 也会被更新。
    • LOCALS 不会被更新,所以局部变量 aLOCALS['a'] 仍然是 Number(1)
    • 现在,再次调用 locals()SINGLETON 被更新。
    • 因为 d 引用的是 SINGLETON,而不是 LOCALS,所以 d 也会改变!

关于这个令人惊讶的细节,为什么 1 是单例而 300 不是,可以查看 https://stackoverflow.com/a/306353

但请不要忘记:数字是不可变的,所以如果你试图将一个数字改为另一个值,你实际上是创建了一个新对象。

结论:

你不能把 Python 2 中 exec 的行为带回到 Python 3(除非你改变你的代码),因为现在没有办法在程序流程之外改变局部变量。

不过,你可以把 Python 3 的行为带到 Python 2,这样你今天可以编写在 Python 3 或 Python 2 中都能正常运行的程序。这是因为在(较新的)Python 2 中,你也可以使用类似函数的参数来使用 exec(实际上,这些是一个 2 或 3 元组),这允许你使用与 Python 3 相同的语法和语义:

exec "code"

(这只在 Python 2 中有效)变成(在 Python 2 和 3 中都有效):

exec("code", globals(), locals())

但要注意,"code" 不再能以这种方式改变局部作用域。另请参见 https://docs.python.org/2/reference/simple_stmts.html#exec

Python 3 中 exec 的变化是好的,因为优化。

在 Python 2 中,你无法在 exec 之间进行优化,因为所有包含不可变内容的局部变量的状态可能会不可预测地改变。这种情况不再发生。现在,exec() 和其他函数一样,遵循常规的函数调用规则。

9

我觉得这是python3的一个bug。

def u():
    exec("a=2")
    print(locals()['a'])
u()

输出“2”。

def u():
    exec("a=2")
    a=2
    print(a)
u()

输出“2”。

但是

def u():
    exec("a=2")
    print(locals()['a'])
    a=2
u()

失败了,显示

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in u
KeyError: 'a'

--- 编辑 ---

还有一个有趣的行为:

def u():
    a=1
    l=locals()
    exec("a=2")
    print(l)
u()
def u():
    a=1
    l=locals()
    exec("a=2")
    locals()
    print(l)
u()

输出

{'l': {...}, 'a': 2}
{'l': {...}, 'a': 1}

而且

def u():
    l=locals()
    exec("a=2")
    print(l)
    print(locals())
u()
def u():
    l=locals()
    exec("a=2")
    print(l)
    print(locals())
    a=1
u()

输出

{'l': {...}, 'a': 2}
{'l': {...}, 'a': 2}
{'l': {...}, 'a': 2}
{'l': {...}}

显然,exec在局部变量上的作用是这样的:

  • 如果在exec中设置了一个变量,并且这个变量是局部变量,那么exec会修改内部字典(就是通过locals()返回的那个字典),而不会把它恢复到原来的状态。调用locals()会更新这个字典(在python文档的第2节中有说明),而在exec中设置的值会被遗忘。需要调用locals()来更新字典并不是python3的bug,因为这是有文档说明的,但这并不直观。此外,在exec中对局部变量的修改不会改变函数的局部变量,这一点是与python2的一个文档差异(文档中说“如果你需要在函数exec()返回后看到代码对局部变量的影响,请传递一个显式的局部字典”),我更喜欢python2的行为。
  • 如果在exec中设置了一个变量,而这个变量之前并不存在,那么exec会修改内部字典,除非这个变量在之后被设置。似乎在locals()更新字典的方式上有一个bug;这个bug使得在exec之后调用locals()可以访问到在exec中设置的值。
56

在Python 2和Python 3中,exec的用法有很大不同。你可能把exec当成一个函数来用,但在Python 2中,它其实是一个语句

因为这个区别,在Python 3中,你不能通过exec来改变函数内部的局部变量,尽管在Python 2中是可以的。即使是之前已经声明的变量也不行。

locals()只会反映局部变量的一个方向。下面的代码在Python 2和3中都不管用:

def foo():
    a = 'spam'
    locals()['a'] = 'ham'
    print(a)              # prints 'spam'

在Python 2中,使用exec语句时,编译器会知道要关闭局部作用域的优化(比如从LOAD_FAST切换到LOAD_NAME,这样可以在局部和全局作用域中查找变量)。而在Python 3中,exec()是一个函数,这个选项就不再可用了,函数的作用域现在总是被优化。

而且,在Python 2中,exec语句会明确地把locals()中找到的所有变量复制回函数的局部变量中,前提是没有提供globalslocals参数。

解决这个问题的正确方法是为你的exec()调用使用一个新的命名空间(一个字典):

def execute(a, st):
    namespace = {}
    exec("b = {}\nprint('b:', b)".format(st), namespace)
    print(namespace['b'])

exec()的文档对此限制说明得很清楚:

注意:默认的locals的行为与下面函数locals()的描述相同:不应该尝试修改默认的locals字典。如果你需要在函数exec()返回后查看代码对局部变量的影响,请传递一个明确的locals字典。

撰写回答