为什么 python 使用中间单元格作为闭包?

Why does python use intermediate cell for closures?

提问人:andrew mamchyn 提问时间:1/23/2023 更新时间:6/11/2023 访问量:72

问:

伙计们,我完全不明白为什么 Python 使用中间单元格进行闭包。例如:

def outer():
   x = "world"
   def inner():
      print(f"Hello {x}")
   return inner

现在 x.outer 和 x.inner(忽略点表示法,这只是为了区分两个变量)都指向同一个中间单元格,而中间单元格又指向包含我们的字符串对象的内存单元格。

我们从这个中间细胞中得到了什么?为什么这两个变量不能直接指向包含我们的字符串对象的内存单元?

从引用计数的角度来看,即使在 outer() 函数完成运行后,我们仍然有 1 的引用计数(因为我们仍然有 x.inner 变量),因此 python 内存管理器将无法清空这个中间单元格。但是,如果这两个变量直接指向包含字符串对象的内存单元,我们将具有相同的引用计数。所以我想这与参考计数无关。

那么,这个中间细胞背后的想法是什么,为什么我们需要使用它呢? 谢谢

闭包 python-closures

评论

0赞 Ralf Ulrich 1/23/2023
你的问题真的不是那么清楚。请改进。我的意思是“内部”只是计算到一个新字符串,对吧?
1赞 DarrylG 1/23/2023
我还没有在 Python 中看到过“中间单元”一词。那是什么意思?
0赞 andrew mamchyn 1/23/2023
@DarrylG 如果我们运行上面的代码,我们将得到以下内容: fn = outer() print(fn.__closure__) --> (<cell at 0x10d1edfd0: str object at 0x1089627b0>,)。正如你现在所看到的,我们的自由变量指向存储在 0x10d1edfd0 的中间单元格,而中间单元又指向包含我们的字符串对象的内存单元。我的问题是,为什么python不直接指向内存单元,而是使用这个中间单元?
0赞 andrew mamchyn 1/23/2023
@RalfUlrich 请看我上面的评论。谢谢。
0赞 DarrylG 1/24/2023
正如 Python 闭包所示,以 Python 目前的方式提供它允许小类的功能。“中间单元格”类似于对象实例中的变量。

答:

0赞 jsbueno 6/11/2023 #1

这个想法对我来说似乎很清楚——如果我能够把它写出来: 在 Python 中,变量本身并不处于固定的内存位置,而是引用放置在该内存位置的任何内容,就像静态语言一样。它们也不直接“包含对内存中对象的引用(指针)”。发生的情况是,它们间接包含此引用,并且这种间接发生方式取决于变量 kind。(读到最后)

首先,让我们理解,如果没有“中间”单元格对象,对内部作用域中变量的赋值也只会改变对象所指向的 - 并且这种值更改永远不会被外部作用域中的人知道:它只会继续引用前一个对象。xxx

对于非局部变量,编译器生成的字节码将向单元格对象读取和写入值,以便该值在共享同一变量的所有变量中始终保持同步。但是对于使用这些值的代码,在 Python 源代码中,它是完全透明的:字节码将始终被构建为加载和设置单元格的值 - 而对于在内部作用域变量中未使用的普通字节码,会发出不同的字节码,这将在不同的区域保存给定变量的值(fast-locals, 或全局变量)。

所以,换句话说:Python 中有 3 种根本不同的变量类型,编译器在编译时知道 (*) 给定的变量是哪种类型 - 它会为每种类型发出不同的字节码。非局部变量获得字节码,字节码使用对象(就好像它是静态语言中的内存位置)来存储自己的值。((*) - 如果在编译时对变量的类型存在歧义,编译器将简单地出错。尽管有遗留的字节码指令可以在所有有效范围内按名称搜索变量,但我认为 Python 3.11 中的代码不再生成这些指令。cell

举例说明上面第二段中的示例 - 检查以下代码:


def a():
   x = 1
   def c():
      nonlocal x
      x = 2
   c()
   print(x)

如果 s 只是“指向一个值”,当它在内部函数中更改时,将无法知道该值已更改。x2c

查看反汇编代码的最后一部分:a

  7          38 LOAD_GLOBAL              1 (NULL + print)
             50 LOAD_DEREF               1 (x)
             52 PRECALL                  1
             56 CALL                     1

当内部函数不使用来自以下变量时,将其与相同的代码进行比较:a

In [4]: 
   ...: def a():
   ...:    x = 1
   ...:    def c():
   ...:       y = 2
   ...:    c()
   ...:    print(x)
...
  7          32 LOAD_GLOBAL              1 (NULL + print)
             44 LOAD_FAST                0 (y)
             46 PRECALL                  1
             50 CALL 

如上所述,Python 使用不同的操作码策略:vs 在这两种情况下:“DEREF”操作码是将值存储到 Cell 对象并从中检索它们的操作码。事实上,我们可以从 Python 代码中检查和访问 Cell 对象,这几乎是一种“奢侈”——如果它是完全不透明的,那么该语言的工作方式也是一样的。LOAD_DEREFLOAD_FAST

至于你对引用计数的关注,我们进入了最后一部分:正如你所看到的,“x”名称只存在于源代码中。在上面的第一种情况下,每当我们使用变量时,只有一个对值的引用:单元格引用的对象。实际上,这可能会让人头晕目眩:Python 变量在执行编程时并不“存在”——它们只“存在”在存储或从中检索值的点——在这些点上,编译器会发出适当的字节码来存储或从适当的容器中检索该值。对于全局变量,该容器是模块字典(回退到模块)。对于局部变量,它通常是当前 Frame 中的一个插槽,由 and 使用(这些插槽的包含在需要时镜像到字典),对于非局部变量或与内部作用域共享的变量,容器是一个 Cell 对象。xglobals()builtinsLOAD_FASTSTORE_FASTlocals()

如果像上面这样的闭包中使用的变量“x”的行为就像是同一个变量,并且仅作为对其值的一个引用,即使有多个内部函数使用同一个单元格,那么所有这一切的最终结果。同时,如果在任何时候我们创建第二个变量,并执行 yxy'。它就像魔术一样工作,但它只是一些逻辑设计。y = x, whatever is the type of (local to the outer or any of the inner functions, nonlocal or global) a second reference to the value ofis created and placed in the appropriate container for