Python 2 和 Python 3 中 exec 函数的行为
下面的代码在 Python2
和 Python3
中输出结果不同:
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
为什么 Python2
在 execute
函数内部把变量 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
比较 d
和 locals()
的 ID 显示它们是同一个对象。但在这种情况下,b
应该和 d['b']
是一样的。我的例子中出了什么问题呢?
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())
,因为d
和locals()
引用的是同一个对象,同一个单例,因为在同一个作用域中只能有一个(在不同的作用域中你会得到不同的对象,但在同一个作用域中你只会看到这个单一的对象)。
- 因此可以观察到,
- 它是
可变的
,因为它是一个普通对象,所以你可以改变它。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
不会被更新,所以局部变量a
或LOCALS['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()
和其他函数一样,遵循常规的函数调用规则。
我觉得这是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
中设置的值。
在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()
中找到的所有变量复制回函数的局部变量中,前提是没有提供globals和locals参数。
解决这个问题的正确方法是为你的exec()
调用使用一个新的命名空间(一个字典):
def execute(a, st):
namespace = {}
exec("b = {}\nprint('b:', b)".format(st), namespace)
print(namespace['b'])
exec()
的文档对此限制说明得很清楚:
注意:默认的locals的行为与下面函数
locals()
的描述相同:不应该尝试修改默认的locals字典。如果你需要在函数exec()
返回后查看代码对局部变量的影响,请传递一个明确的locals字典。