在 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 的规范表明,您在函数内部分配的任何变量都是局部变量。当然,您可以通过使用 global
或 nonlocal
来更改此默认设置。但是一写z = ...
,z
就变成局部变量了。另一方面,如果您的代码中只有 x = z
,则编译器会假定 z
是一个全局变量。这就是为什么行 z = 1
造成了所有的不同:它将 z
标记为一个局部变量,它在 fast
数组中占有一席之地。
关于 exec()
:一般来说,编译器无法真正知道 exec()
将要执行什么代码(在你的情况下它可以使用字符串文字,但因为这是一种罕见且无趣的情况,无论如何它都不会尝试)。因此,编译器不知道 exec()
中的代码可能访问哪些局部(或全局)变量,并且无法将其包含在计算局部变量数组应该有多大时。
顺便说一句:局部变量是在数组中而不是在适当的字典中管理的,这就是为什么可能会引发 UnboundLocalError
而不是 NameError
的原因。在局部变量的情况下,Python 解释器实际上识别名称并确切知道在哪里可以找到它的值(在上面提到的 fast
数组中)。但是如果该条目是 null
,它就不能 return 有意义的东西,因此会引发 UnboundLocalError
。然而,对于全局名称,Python 确实会在全局变量和内置词典中搜索具有给定名称的变量。在这种情况下,所请求名称的变量可能确实不存在。
我正在使用 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 的规范表明,您在函数内部分配的任何变量都是局部变量。当然,您可以通过使用 global
或 nonlocal
来更改此默认设置。但是一写z = ...
,z
就变成局部变量了。另一方面,如果您的代码中只有 x = z
,则编译器会假定 z
是一个全局变量。这就是为什么行 z = 1
造成了所有的不同:它将 z
标记为一个局部变量,它在 fast
数组中占有一席之地。
关于 exec()
:一般来说,编译器无法真正知道 exec()
将要执行什么代码(在你的情况下它可以使用字符串文字,但因为这是一种罕见且无趣的情况,无论如何它都不会尝试)。因此,编译器不知道 exec()
中的代码可能访问哪些局部(或全局)变量,并且无法将其包含在计算局部变量数组应该有多大时。
顺便说一句:局部变量是在数组中而不是在适当的字典中管理的,这就是为什么可能会引发 UnboundLocalError
而不是 NameError
的原因。在局部变量的情况下,Python 解释器实际上识别名称并确切知道在哪里可以找到它的值(在上面提到的 fast
数组中)。但是如果该条目是 null
,它就不能 return 有意义的东西,因此会引发 UnboundLocalError
。然而,对于全局名称,Python 确实会在全局变量和内置词典中搜索具有给定名称的变量。在这种情况下,所请求名称的变量可能确实不存在。