在 python3 中调用 locals() 时发生了什么?

What happened when invoking locals() in python3?

我正在使用 Python3.7.2

让我感到困惑的代码如下所示:

def foo(cond):
    if cond:
        z = 1
        return z
    else:
        loc = locals()
        x = 1
        locals()['y'] = 2
        exec("z = x + y")
        print(loc)
        print(locals())

foo(False)

这是它打印的结果:

{'cond': False, 'loc': {...}, 'x': 1, 'y': 2, 'z': 3}
{'cond': False, 'loc': {...}, 'x': 1, 'y': 2}

exec函数改变了局部命名空间,但是当我执行locals()时,变量z消失了。

但是,如果我这样更改代码:

def foo(cond):
    if cond:
        return 1
    else:
        loc = locals()
        x = 1
        locals()['y'] = 2
        exec("z = x + y")
        print(loc)
        print(locals())

foo(False)

结果将是:

{'cond': False, 'loc': {...}, 'x': 1, 'y': 2, 'z': 3}
{'cond': False, 'loc': {...}, 'x': 1, 'y': 2, 'z': 3}

我真的很困惑。 :(

将您的代码与以下内容进行比较:

def foo(cond):
    if cond:
        return 1
    else:
        loc = locals().copy()
        # ——————————————^
        x = 1
        locals()['y'] = 2
        exec("z = x + y")
        print(loc)
        print(locals())

foo(False)

它产生

{'cond': False}
{'cond': False, 'loc': {'cond': False}, 'x': 1, 'y': 2, 'z': 3}

如您所料。原因是locals()是一个引用,不是一个值。

简而言之:由 locals() 编辑的字典 return 只是真正局部变量的 副本 — 存储在数组中,而不是在字典里。您可以操纵 locals() 词典,随意添加或删除条目。 Python 并不关心,因为这只是它所使用的局部变量的副本。但是,每次调用 locals() 时,Python 会将所有局部变量的当前值复制到字典中,替换您或 exec() 之前输入的任何其他内容。因此,如果 z 是一个适当的局部变量(如代码的第一个版本,但不是第二个版本),locals() 将恢复其当前值,恰好是 "not set" 或 "undefined".


确实,概念上Python将局部变量存储在字典中,可以通过函数locals()检索。然而,在内部,它确实是两个数组。本地人的名字是代码本身的一个属性,因此在代码对象中存储为 code.co_varnames。但是,它们的值存储在 frame 中作为 fast(无法从 Python 代码访问)。您可以分别在 inspect and dis 模块中找到有关 co_varnames 等的更多信息。

内置函数locals()实际上在当前frame对象上调用了一个方法fastToLocals()。如果我们要在 Python 中使用其 fastToLocals 方法编写框架,它可能看起来像这样(我在这里省略了很多 很多 细节):

class frame:
    def __init__(self, code):
        self.__locals_dict = None
        self.f_code = code
        self.__fast = [0] * len(code.co_varnames)

    def fastToLocals(self):
        if self.__locals_dict is None:
            self.__locals_dict = {}
        for (key, value) in zip(self.f_code.co_varnames, self.__fast):
            if value is not null:             # Remember: it's actually C-code
                self.__locals_dict[key] = value
            else:
                del self.__locals_dict[key]   # <- Here's the magic
        return self.__locals_dict

def locals():
    frame = sys._getframe(1)
    frame.fastToLocals()

通俗地说:调用locals()时得到的字典缓存在当前帧中。重复调用 locals() 将为您提供相同的字典(上面代码中的 __locals_dict)。但是,每次调用 locals() 时,框架都会使用局部变量当时的当前值更新该字典中的条目。如前所述 here,当未设置局部变量时,__locals_dict 中的条目将被删除 。这就是它的全部意义。

在您的第一个版本的代码中,第三行表示 z = 1,这使得 z 成为局部变量。然而,在 else 分支中,局部变量 z 未设置(即会引发 UnboundLocalError),因此从 __locals_dict 中删除。在您的代码的第二个版本中,没有对 z 的赋值,因此它 不是 局部变量并且 locals() 函数不关心它。

这组局部变量实际上是由编译器固定的。这意味着您不能在运行时添加或删除局部变量。这是 exec() 的问题,因为您在这里显然使用 exec() 来定义局部变量 z。 Python的出路是说exec()z作为局部变量存储在_locals_dict字典中,虽然它不能将它放入这个字典后面的数组中。

结论:局部变量的值实际上是存储在一个数组中,而不是locals()编辑的return字典中。当调用 locals() 时,字典会更新为从字典中获取的真实当前值。 exec(),另一方面,只能将其局部变量存储在字典中,而不能存储在数组中。如果 z 是一个合适的局部变量,它将被调用 locals() 的当前值(即 "does not exist")覆盖。如果 z 不是一个合适的局部变量,它将原封不动地留在字典中。


Python 的规范表明,您在函数内部分配的任何变量都是局部变量。当然,您可以通过使用 globalnonlocal 来更改此默认设置。但是一写z = ...z就变成局部变量了。另一方面,如果您的代码中只有 x = z,则编译器会假定 z 是一个全局变量。这就是为什么行 z = 1 造成了所有的不同:它将 z 标记为一个局部变量,它在 fast 数组中占有一席之地。

关于 exec():一般来说,编译器无法真正知道 exec() 将要执行什么代码(在你的情况下它可以使用字符串文字,但因为这是一种罕见且无趣的情况,无论如何它都不会尝试)。因此,编译器不知道 exec() 中的代码可能访问哪些局部(或全局)变量,并且无法将其包含在计算局部变量数组应该有多大时。


顺便说一句:局部变量是在数组中而不是在适当的字典中管理的,这就是为什么可能会引发 UnboundLocalError 而不是 NameError 的原因。在局部变量的情况下,Python 解释器实际上识别名称并确切知道在哪里可以找到它的值(在上面提到的 fast 数组中)。但是如果该条目是 null,它就不能 return 有意义的东西,因此会引发 UnboundLocalError。然而,对于全局名称,Python 确实会在全局变量和内置词典中搜索具有给定名称的变量。在这种情况下,所请求名称的变量可能确实不存在。