在函数中调用locals()不直观?

7 投票
4 回答
3442 浏览
提问于 2025-04-18 04:36

这可能是个基础问题,但我希望能帮助我理解命名空间。一个好的解释可以逐步讲解当函数定义被执行时发生了什么,以及当函数对象被执行时又发生了什么。递归可能让事情变得复杂。

结果对我来说并不明显;我本来以为:

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 个回答

1

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() 之前定义的变量。

4

我很喜欢你的问题,真是个好问题。

是的,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 也会间接改变,因为 ab 都指向内存中的同一个数据结构。

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())
5

让我们来运行这段代码:

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

看到了吗?所以,这就是为什么它们的值完全相同,因为它们都指向 printlocals() 的值。

如果你再次以某种方式改变 locals(),然后运行打印语句,会打印出什么呢?没错,会是 locals() 的新值,打印三次。

0

我最初的问题是,“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)

撰写回答