在Python 3中动态创建变量名/ 理解exec/eval/locals

10 投票
2 回答
6704 浏览
提问于 2025-04-18 15:37

首先,我想说我看过很多关于动态命名变量的讨论,但大多数都是针对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 个回答

5

关于 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())

传入不同的全局和局部作用域字典可能会产生“有趣”的行为。

如果你在连续的 execeval 调用中传入相同的字典,那么它们会共享同一个作用域,这就是为什么你的 eval 能工作的原因(它隐式地使用了 locals() 字典)。

关于动态变量名

如果你通过字符串设置变量名,那么从字符串获取值有什么问题呢(也就是字典的做法)?换句话说,为什么你会想要设置 locals()['K'] 然后再访问 K?如果 K 在你的源代码中,那它其实并不是一个动态设置的名字……所以用字典来处理更合适。

21

当你不明白为什么某些东西在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_FASTLOAD_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()字典,而你发现这些变化并不总是被后面的代码看到。

撰写回答