在函数中调用locals()不直观?
这可能是个基础问题,但我希望能帮助我理解命名空间。一个好的解释可以逐步讲解当函数定义被执行时发生了什么,以及当函数对象被执行时又发生了什么。递归可能让事情变得复杂。
结果对我来说并不明显;我本来以为:
locals_1会包含变量;
locals_2会包含变量和locals_1;
而locals_3会包含变量、locals_1和locals_2。
# A function calls locals() several times, and returns them ...
def func():
var = 'var!'
locals_1 = locals()
locals_2 = locals()
locals_3 = locals()
return locals_1, locals_2, locals_3
# func is called ...
locals_1, locals_2, locals_3 = func()
# display results ...
print 'locals_1:', locals_1
print 'locals_2:', locals_2
print 'locals_3:', locals_3
这里是结果:
locals_1: {'var': 'var!', 'locals_1': {...}, 'locals_2': {...}}
locals_2: {'var': 'var!', 'locals_1': {...}, 'locals_2': {...}}
locals_3: {'var': 'var!', 'locals_1': {...}, 'locals_2': {...}}
看起来的规律是,当调用locals (n) 次时,所有返回的locals字典都是一样的,并且它们都包含前(n-1)个locals字典。
有人能解释一下吗?
更具体地说:
为什么locals_1会包含它自己?
为什么locals_1会包含locals_2?locals_1是在func创建时被赋值,还是在执行时被赋值?
而为什么locals_3没有被包含在任何地方?
"{...}"是否表示一种“无尽的递归”?有点像那些面对面镜子的照片?
4 个回答
frostnational 和 Jan Vlcinsky 已经很好地解释了背后发生了什么。这里再补充一点,帮助你实现你最初期待的行为。你可以使用 copy
方法来创建一个 locals()
字典的副本。这个副本在 locals()
更新时不会改变,所以它包含了你想要的“快照”:
In [1]: def func():
...: var = 'var!'
...: locals1 = locals().copy()
...: locals2 = locals().copy()
...: locals3 = locals().copy()
...: return locals1, locals2, locals3
...:
In [2]: locals1, locals2, locals3 = func()
In [3]: locals1
Out[3]: {'var': 'var!'}
In [4]: locals2
Out[4]: {'locals1': {'var': 'var!'}, 'var': 'var!'}
In [5]: locals3
Out[5]:
{'locals1': {'var': 'var!'},
'locals2': {'locals1': {'var': 'var!'}, 'var': 'var!'},
'var': 'var!'}
正如预期的那样,每个副本只包含在调用 locals()
之前定义的变量。
我很喜欢你的问题,真是个好问题。
是的,locals()
有点神奇,但用你这种方法,你迟早会理解它,并且会喜欢上它。
关键概念
字典是通过引用赋值,而不是通过值赋值
In [1]: a = {"alfa": 1, "beta": 2}
In [2]: b = a
In [3]: b
Out[3]: {'alfa': 1, 'beta': 2}
In [4]: b["gama"] = 3
In [5]: b
Out[5]: {'alfa': 1, 'beta': 2, 'gama': 3}
In [6]: a
Out[6]: {'alfa': 1, 'beta': 2, 'gama': 3}
如你所见,当 b
被修改时,a
也会间接改变,因为 a
和 b
都指向内存中的同一个数据结构。
locals()
返回一个包含所有局部变量的字典
补充说明:解释了这个字典何时更新
所以在调用 locals()
的时候,所有存在的局部变量都在这里。如果你后续再次调用 locals()
,这个字典会在调用的那一刻更新。
回答你的问题
为什么 locals_1 包含它自己?
因为 locals_1
是一个指向所有局部定义变量字典的引用。一旦 locals_1
成为局部命名空间的一部分,它就会成为 locals()
返回的字典的一部分。
为什么 locals_1 包含 locals_2?locals_1 是在函数创建时赋值,还是在执行时赋值?
和之前的问题一样的答案。
为什么 locals_3 不包含在任何地方?
这是你问题中最难的部分。经过一些研究,我找到了一篇关于这个主题的优秀文章:http://nedbatchelder.com/blog/201211/tricky_locals.html
事实是,locals()
返回一个字典,里面包含所有局部变量的引用。但棘手的地方在于,它并不是直接的那个结构,而是一个字典,只有在调用 locals()
的时候才会更新。
这就解释了为什么你的结果中缺少 locals_3。所有结果都指向同一个字典,但在你引入 locals_3
变量后,它并没有更新。
当我在返回之前添加了另一个打印 locals()
的语句时,我发现它在那里,没有它就没有。
唉。
"{...}" 表示“无限递归”吗?就像那些面对面镜子的照片?
我会理解为“还有更多内容”。但我觉得你说得对,这确实是打印递归数据结构的解决方案。没有这样的解决方案,字典就无法在有限的时间内真正打印出来。
附加内容 - 在 string.format() 中使用 **locals()
有一种用法,locals()
可以大大简化你的代码,就是在 string.format()
中。
name = "frost"
surname = "national"
print "The guy named {name} {surname} got great question.".format(**locals())
让我们来运行这段代码:
def func():
var = 'var!'
locals_1 = locals()
print(id(locals_1), id(locals()), locals())
locals_2 = locals()
print(id(locals_2), id(locals()), locals())
locals_3 = locals()
print(id(locals_3), id(locals()), locals())
return locals_1, locals_2, locals_3
func()
输出结果会是这样的:
44860744 44860744 {'locals_1': {...}, 'var': 'var!'}
44860744 44860744 {'locals_2': {...}, 'locals_1': {...}, 'var': 'var!'}
44860744 44860744 {'locals_2': {...}, 'locals_3': {...}, 'locals_1': {...}, 'var': 'var!'}
这里的 locals()
按照预期增长了,但你实际上是把 引用 赋值给了 locals()
,而不是把 值 赋给每个变量。
每次赋值后,locals()
会改变,但引用不会,所以每个变量都指向同一个对象。在我的输出中,所有对象的 id
都是相等的,这就是证明。
更详细的解释
这些变量都与那个对象有相同的链接(引用)。基本上,Python 中的所有变量都是引用(类似于指针的概念)。
locals_1 locals_2 locals_3
\ | /
\ | /
V V V
---------------------------------------------
| single locals() object |
---------------------------------------------
它们根本不知道 locals()
现在的值是什么,它们只知道在需要的时候(当变量在某处被使用时)去哪里获取这个值。对 locals()
的更改不会影响这些变量。
在你函数的最后,你返回了三个变量,这就是当你打印它们时发生的事情:
print(locals_N) -> 1. Get object referenced in locals_N
2. Return the value of that object
看到了吗?所以,这就是为什么它们的值完全相同,因为它们都指向 print
时 locals()
的值。
如果你再次以某种方式改变 locals()
,然后运行打印语句,会打印出什么呢?没错,会是 locals()
的新值,打印三次。
我最初的问题是,“locals()到底是什么?”
以下是我目前的(猜测性的)理解,用简单的Python语言表达:
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Python中的locals()是什么?
每个局部命名空间都有自己的命名空间表,可以通过locals
这个内置函数查看它的全部内容。
(命名空间表实际上就像一个字典,里面存储着“标识符”:对象的条目;每个条目的键是分配给对象的名称,形式为字符串。)
在非全局的环境中调用locals
时,它会返回解释器当前局部命名空间表的唯一表示:一个“动态的”、始终保持最新的、特殊的、类似字典的对象。
这不是一个简单的字典,也不是实际的名称表,但它实际上是“活”的,任何时候被引用时都会立即从实时表中更新(当追踪开启时,它会在每条语句后更新)。
当退出作用域时,这个对象会消失,并在下次调用locals
时为当前作用域重新创建。
(在全局(模块)级别调用locals
时,它会返回globals()
,这是Python的全局命名空间表示,可能有不同的特性。)
所以,L = locals()
将名称L
绑定到这个局部命名空间表的“替代品”;之后,每次引用L
时,这个对象都会被刷新并返回。
而在同一作用域内绑定到locals()
的任何其他名称都会成为这个对象的别名。
注意,L
被赋值为locals()
,因此它必然成为一个“无限递归”的对象(显示为字典{...}
),这可能对你来说重要,也可能不重要。不过,你可以随时对L
做一个简单的字典复制。
locals()
的一些属性,比如keys
,也会返回简单的对象。
要在函数内捕获locals()
的“原始”快照,可以使用一种不进行任何局部赋值的技巧;例如,将副本作为参数传递给一个函数,然后将其保存到文件中。
关于L
及其行为有一些细节;它包含来自函数块的自由变量,但不包括类的变量,并且文档警告不要尝试修改L
的内容(这样可能不再“反映”名称表)。
它可能只应该被读取(复制等)。
(为什么locals()
被设计为“实时”的,而不是“快照”,是另一个话题。)
总结一下:
locals()
是一个独特的、特殊的对象(以字典形式存在);它是Python当前局部命名空间表的实时表示(而不是一个静态快照)。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
那么,得到我预期结果的一种方法是在每一步生成locals()的副本(这里使用字典的copy方法):
# A function copies locals() several times, and returns each result ...
def func():
var = 'var!'
locals_1 = locals().copy()
locals_2 = locals().copy()
locals_3 = locals().copy()
return locals_1, locals_2, locals_3
func被调用,返回结果被显示:
locals_1: {'var': 'var!'}
locals_2: {'var': 'var!', 'locals_1': {'var': 'var!'}}
locals_3: {'var': 'var!', 'locals_1': {'var': 'var!'}, 'locals_2':{'var':'var!','locals_1': {'var': 'var!'}}}
返回结果是简单的字典对象,捕获了局部命名空间的逐步变化。
这正是我想要的。
复制locals()
(这里是“L”)的其他可能方法有dict(L)
、copy.copy(L)
和copy.deepcopy(L)
。