在Python 3中动态创建变量名/ 理解exec/eval/locals
首先,我想说我看过很多关于动态命名变量的讨论,但大多数都是针对Python 2的,或者假设你在使用类。而且,我也看过关于exec函数在Python 2和Python 3中的行为的内容。
我知道在99%的情况下,创建动态命名变量并不是个好主意,使用字典才是更好的选择,但我只是想知道在Python 3中,这种做法是否仍然可能,以及exec和locals到底是怎么工作的。
我想展示一段示例代码来说明我的问题(fibonacci计算斐波那契数,ListOfLetters提供["A", "B", ...]):
def functionname():
for index, buchstabe in enumerate(ListOfLetters.create_list("A", "K"), 1):
exec("{} = {}".format(buchstabe, fibonacci(index)) ) #A = 1, B = 1, C = 2, D = 3, E = 5,...
print(index, buchstabe, eval(buchstabe)) #works nicely, e.g. prints "4 D 3"
print(locals()) #pritns all locals: {'B': 1, 'A': 1, 'index': 11, 'C': 2, 'H': 21, 'K': 89, ...
print(locals()['K']) #prints 89 as it should
print(eval("K")) #prints 89 as it should
print(K) #NameError: name 'K' is not defined
根据我目前的理解,locals()
的行为有些不一致,因为它包含了通过exec()
添加的变量名,但这些变量在函数中并不可用。
如果有人能解释一下这个情况,并告诉我这是设计使然还是语言中的真正不一致,我将非常感激。是的,我知道locals
不应该被修改,但我并没有修改它,我只是调用了exec()
……
2 个回答
关于 exec/eval/locals 的问题
至少在 CPython 实现中,对 locals()
字典的修改并不会真正改变局部作用域中的变量名,所以它是建议只读的。你可以修改这个字典,并且能在字典对象中看到你的更改,但实际上局部作用域并没有改变。
exec()
函数可以接收两个可选的字典参数,一个是全局作用域,另一个是局部作用域。默认情况下,它使用 globals()
和 locals()
,但是因为对 locals()
的更改在字典之外并不“真实”,所以 exec()
只有在 globals() 是 locals()
的情况下才会影响“真实”的局部作用域,也就是说在模块中,而不是在任何函数内部。(所以在你的情况下,它失败了,因为它在一个函数的作用域内)。
在这种情况下,使用 exec()
的“更好”方法是传入你自己的字典,然后在那个字典的值上进行操作。
def foo():
exec_scope = {}
exec("y = 2", exec_scope)
print(exec_scope['y'])
foo()
在这里,exec_scope
被用作 exec
的全局和局部作用域,执行完 exec
后,它将包含 {'y': 2, '__builtins__': __builtins__}
(如果没有内置对象,它会自动插入)。
如果你想访问更多的全局变量,可以使用 exec_scope = dict(globals())
。
传入不同的全局和局部作用域字典可能会产生“有趣”的行为。
如果你在连续的 exec
或 eval
调用中传入相同的字典,那么它们会共享同一个作用域,这就是为什么你的 eval
能工作的原因(它隐式地使用了 locals()
字典)。
关于动态变量名
如果你通过字符串设置变量名,那么从字符串获取值有什么问题呢(也就是字典的做法)?换句话说,为什么你会想要设置 locals()['K']
然后再访问 K
?如果 K
在你的源代码中,那它其实并不是一个动态设置的名字……所以用字典来处理更合适。
当你不明白为什么某些东西在Python中这样工作的时,通常可以把你困惑的行为放到一个函数里,然后用dis
模块把它拆解成字节码来看看。
我们先从你代码的一个简单版本开始:
def foo():
exec("K = 89")
print(K)
如果你运行foo()
,你会得到和你更复杂的函数一样的错误:
>>> foo()
Traceback (most recent call last):
File "<pyshell#167>", line 1, in <module>
foo()
File "<pyshell#166>", line 3, in foo
print(K)
NameError: name 'K' is not defined
让我们拆解一下,看看原因:
>>> import dis
>>> dis.dis(foo)
2 0 LOAD_GLOBAL 0 (exec)
3 LOAD_CONST 1 ('K = 89')
6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
9 POP_TOP
3 10 LOAD_GLOBAL 1 (print)
13 LOAD_GLOBAL 2 (K)
16 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
19 POP_TOP
20 LOAD_CONST 0 (None)
23 RETURN_VALUE
你需要关注的操作是标记为“13”的那一行。这是编译器在处理函数最后一行(print(K)
)时查找K
的地方。它使用了LOAD_GLOBAL
这个操作码,但失败了,因为“K”并不是一个全局变量名,而是我们locals()
字典中的一个值(是通过exec
调用添加的)。
如果我们能让编译器把K
当作一个局部变量来看待(在运行exec
之前给它一个值),这样它就知道不去找一个不存在的全局变量了,这样会怎样呢?
def bar():
K = None
exec("K = 89")
print(K)
如果你运行这个函数,它不会给你错误,但你也不会看到预期的值被打印出来:
>>> bar()
None
让我们再拆解一下,看看原因:
>>> dis.dis(bar)
2 0 LOAD_CONST 0 (None)
3 STORE_FAST 0 (K)
3 6 LOAD_GLOBAL 0 (exec)
9 LOAD_CONST 1 ('K = 89')
12 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
15 POP_TOP
4 16 LOAD_GLOBAL 1 (print)
19 LOAD_FAST 0 (K)
22 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
25 POP_TOP
26 LOAD_CONST 0 (None)
29 RETURN_VALUE
注意“3”和“19”使用的操作码。Python编译器使用STORE_FAST
和LOAD_FAST
把局部变量K
的值放到槽0里,然后再取出来。使用编号槽比从像locals()
这样的字典中插入和取值要快得多,这就是为什么Python编译器在函数中处理所有局部变量访问时都这么做。你不能通过修改locals()
返回的字典来覆盖槽中的局部变量(就像exec
那样,如果你没有给它传一个字典作为命名空间)。
实际上,让我们尝试第三个版本的函数,这次我们在K
被定义为普通局部变量时再次查看locals
:
def baz():
K = None
exec("K = 89")
print(locals())
这次你也不会在输出中看到89
!
>>> baz()
{"K": None}
你在locals()
中看到旧的K
值的原因在于函数的文档中有解释:
更新并返回一个表示当前局部符号表的字典。
局部变量K
的值存储的槽并没有被exec
语句改变,它只修改了locals()
字典。当你再次调用locals()
时,Python会用槽中的值“更新”字典,替换掉exec
存储的值。
这就是文档中接着说的原因:
注意:这个字典的内容不应该被修改;更改可能不会影响解释器使用的局部和自由变量的值。
你的exec
调用正在修改locals()
字典,而你发现这些变化并不总是被后面的代码看到。