在python 3中的函数中创建动态命名的变量/在python 3中理解exec/eval/locals

OBu*_*OBu 8 python python-3.x

首先,让我说我在创建动态命名变量时阅读了许多具有类似主题的线程,但它们主要与Python 2相关,或者它们假设您正在使用类.是的,我在Python 2和Python 3中阅读了exec函数的行为.

我也知道在99%的时间内创建动态命名的变量是一个坏主意,字典是获得的方式,但我只想知道它是否仍然可行以及exec和locals在python 3中的运行方式.

我想展示一些示例代码来说明我的问题(斐波那契计算斐波纳契数,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
Run Code Online (Sandbox Code Playgroud)

所以至少在我目前的理解中,行为存在一些不一致locals(),因为它包含添加的变量名,exec()但变量在函数中不可用.

如果有人可以解释这一点并告诉我这是设计还是语言中真正的不一致,我会很高兴.是的,我知道locals不应该修改,但我没有修改它,我正在呼唤exec()......

Blc*_*ght 19

如果你不确定为什么某些东西会像在Python中那样工作,它通常可以帮助你把你在一个函数中混淆的行为放在一起,然后用Python dis模块中的Python字节码来反汇编它.

让我们从更简单的代码版本开始:

def foo():
    exec("K = 89")
    print(K)
Run Code Online (Sandbox Code Playgroud)

如果您运行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
Run Code Online (Sandbox Code Playgroud)

让我们拆开它,看看为什么:

>>> 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
Run Code Online (Sandbox Code Playgroud)

您需要注意的操作是标有"13"的操作.这是编译器处理K函数(print(K))的最后一行内查找的位置.它正在使用LOAD_GLOBAL操作码,因为"K"不是全局变量名,而是它在我们的locals()dict中的值(由exec调用添加).

如果我们说服编译器将其K视为局部变量(通过在运行之前给它一个值exec),那么它会知道不会查找不存在的全局变量吗?

def bar():
    K = None
    exec("K = 89")
    print(K)
Run Code Online (Sandbox Code Playgroud)

如果你运行它,这个函数不会给你一个错误,但你不会得到打印出来的预期值:

>>> bar()
None
Run Code Online (Sandbox Code Playgroud)

让我们拆解看看为什么:

>>> 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
Run Code Online (Sandbox Code Playgroud)

请注意"3"和"19"使用的操作码.Python编译器使用STORE_FAST并将LOAD_FAST局部变量的值K放入插槽0,然后将其取回.使用带编号的槽比从字典中插入和获取值要快得多locals(),这就是Python编译器为函数中的所有局部变量访问所做的事情.您不能通过修改返回的字典来覆盖插槽中的局部变量locals()(exec如果您没有将其传递给用于其命名空间的字典).

实际上,让我们尝试我们函数的第三个版本,locals当我们K定义为常规局部变量时,我们再次查看:

def baz():
    K = None
    exec("K = 89")
    print(locals())
Run Code Online (Sandbox Code Playgroud)

89这次你也不会在输出中看到!

>>> baz()
{"K": None}
Run Code Online (Sandbox Code Playgroud)

函数的文档中解释了您看到旧K值的原因:locals()

更新并返回表示当前本地符号表的字典.

K存储局部变量值的槽未被exec语句更改,该语句仅修改locals()dict.locals()再次调用时,Python使用插槽中的值"更新[s]"字典,替换存储在其中的值exec.

这就是为什么文档继续说:

注意:不应修改此词典的内容; 更改可能不会影响解释器使用的本地和自由变量的值.

你的exec电话正在修改locals()字典,你发现后来的代码并不总能看到它的变化.


Jas*_*n S 5

关于exec/eval/locals问题

至少在CPython实现中,对locals()字典的修改实际上并没有改变本地范围内的名称,这就是为什么它只是以只读方式使用.您可以更改它,并且可以在字典对象中查看更改,但不会更改实际的本地范围.

exec()采用两个可选的字典参数,一个全局范围和一个本地范围.它默认为globals()locals(),但由于更改locals()不是字典之外的"真实",exec()只会影响"实际"本地范围globals() is locals(),即在任何函数之外的模块中.(所以在你的情况下,它失败了,因为它在函数范围内).

exec()在这种情况下使用"更好"的方法是传入自己的字典,然后对其中的值进行操作.

def foo():
    exec_scope = {}
    exec("y = 2", exec_scope)
    print(exec_scope['y'])
foo()
Run Code Online (Sandbox Code Playgroud)

在这种情况下,exec_scope用作它的全局和局部范围exec,并在exec它包含之后{'y': 2, '__builtins__': __builtins__}(如果不存在则为你插入内置函数)

如果你想要访问更多全局变量,你可以做到exec_scope = dict(globals()).

传递不同的全局和局部范围词典可以产生"有趣"的行为.

如果你将相同的字典传递给对execor的连续调用eval,那么它们具有相同的范围,这就是你eval工作的原因(它隐含地使用了locals()字典).

关于动态变量名

如果从字符串中设置名称,那么将值作为字符串(即字典的作用)会有什么错误?换句话说,为什么设置locals()['K']然后访问K?如果K在你的源中,它不是一个真正的动态设置名称...因此,字典.