Python 闭包是如何实现的?

6

我对Python如何实现闭包感兴趣?

为了举例说明,请考虑以下内容:

def closure_test():
    x = 1
    def closure():
        nonlocal x
        x = 2
        print(x)
    return closure

closure_test()()

这里,函数closure_test有一个本地变量x,被嵌套函数closure捕获。

当我运行程序时,得到以下输出:

2

当我看到函数closure_test的反汇编时,
  2           0 LOAD_CONST               1 (1)
              2 STORE_DEREF              0 (x)

  3           4 LOAD_CLOSURE             0 (x)
              6 BUILD_TUPLE              1
              8 LOAD_CONST               2 (<code object closure at 0x7f14ac3b9500, file "<string>", line 3>)
             10 LOAD_CONST               3 ('closure_test.<locals>.closure')
             12 MAKE_FUNCTION            8 (closure)
             14 STORE_FAST               0 (closure)

  7          16 LOAD_FAST                0 (closure)
             18 RETURN_VALUE

Disassembly of <code object closure at 0x7f14ac3b9500, file "<string>", line 3>:
  5           0 LOAD_CONST               1 (2)
              2 STORE_DEREF              0 (x)

  6           4 LOAD_GLOBAL              0 (print)
              6 LOAD_DEREF               0 (x)
              8 CALL_FUNCTION            1
             10 POP_TOP
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE

我看到了指令 STORE_DEREF, LOAD_DEREF, MAKE_FUNCTIONLOAD_CLOSURE,如果我不使用函数和闭包编写整个程序,我就无法理解它们。

我认为这些指令是使用闭包所需的指令。

但是 Python 是如何实现这一点的呢?它如何从封闭函数的本地变量表中获取变量?在捕获变量之后,它存在哪里?函数如何访问已捕获的变量?

我想要完全理解它的底层原理。

谢谢。

1个回答

5

概述

Python并不像C或Java这样的静态类型语言一样直接使用变量,而是使用名称并将对象实例与它们标记

在您的示例中,closure只是一个带有该名称的函数实例

在这里真正发挥作用的是nonlocal,它导致LOAD_CLOSUREBUILD_TUPLE被如何时检查非本地变量的存在?所述使用并进一步在Python中如何定义自由变量?中引用x,而不是内部函数字面上命名为closure

  3           4 LOAD_CLOSURE             0 (x)

关于 nonlocal

对于您的情况,nonlocal 断言在编译时,x 存在于外部作用域(全局变量除外),但实际上它是多余的,因为它在其他地方没有被使用。 文档
最初我写道这是由于重新声明而多余的,但这是不正确的 - nonlocal 防止 重复使用名称,但 x 只是没有在其他任何地方显示,因此效果并不明显
我添加了第三个示例,其中包含一个非常丑陋的生成器,以说明其效果。

使用全局变量的示例(请注意,SyntaxError 在编译时而不是运行时发生)

>>> x = 3
>>> def closure_test():
...     def closure():
...         nonlocal x
...         print(x)
...     return closure
...
  File "<stdin>", line 3
SyntaxError: no binding for nonlocal 'x' found
>>> def closure_test():
...     def closure():
...         print(x)
...     return closure
...
>>> closure_test()()
3

SyntaxError与无效本地变量使用相关的示例

>>> def closure_test():
...     def closure():
...         nonlocal x
...         x = 2
...         print(x)
...     return closure
...
  File "<stdin>", line 3
SyntaxError: no binding for nonlocal 'x' found
>>> def closure_test():
...     x = 1
...     def closure():
...         x = 2
...         nonlocal x
...         print(x)
...     return closure
...
  File "<stdin>", line 5
SyntaxError: name 'x' is assigned to before nonlocal declaration

使用nonlocal来设置外部变量的示例
(请注意,这种方法表现不佳,因为将yieldtry:finally包装起来以更正常的方式显示在closure实际调用之前)

>>> def closure_test():
...     x = 1
...     print(f"x outer A: {x}")
...     def closure():
...         nonlocal x
...         x = 2
...         print(f"x inner: {x}")
...     yield closure
...     print(f"x outer B: {x}")
...
>>> list(x() for x in closure_test())
x outer A: 1
x inner: 2
x outer B: 2
[None]

原始示例没有使用nonlocal(请注意没有BUILD_TUPLELOAD_CLOSURE!)

>>> def closure_test():
...     x = 1
...     def closure():
...         x = 2
...         print(x)
...     return closure
...
>>>
>>> import dis
>>> dis.dis(closure_test)
  2           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (x)

  3           4 LOAD_CONST               2 (<code object closure at 0x10d8132f0, file "<stdin>", line 3>)
              6 LOAD_CONST               3 ('closure_test.<locals>.closure')
              8 MAKE_FUNCTION            0
             10 STORE_FAST               1 (closure)

  6          12 LOAD_FAST                1 (closure)
             14 RETURN_VALUE

Disassembly of <code object closure at 0x10d8132f0, file "<stdin>", line 3>:
  4           0 LOAD_CONST               1 (2)
              2 STORE_FAST               0 (x)

  5           4 LOAD_GLOBAL              0 (print)
              6 LOAD_FAST                0 (x)
              8 CALL_FUNCTION            1
             10 POP_TOP
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE

关于字节码及简单比较

为了简化示例,去除所有名称后,它只是

>>> import dis
>>> dis.dis(lambda: print(2))
 ​1           0 LOAD_GLOBAL              0 (print)
             ​2 LOAD_CONST               1 (2)
             ​4 CALL_FUNCTION            16 RETURN_VALUE

其余的字节码只是在移动名称。
  • x 表示 12
  • closureclosure_test.<locals>.closure 表示内部函数(位于某个内存地址)
  • print 字面意思为打印函数
  • None 字面意思为单例None

特定的DIS操作码


可以使用 dis.show_code() 查看常量、名称和自由变量。
>>> dis.show_code(closure_test)
Name:              closure_test
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  1
Stack size:        3
Flags:             OPTIMIZED, NEWLOCALS
Constants:
  ​0: None1: 12: <code object closure at 0x10db282f0, file "<stdin>", line 3>
  ​3: 'closure_test.<locals>.closure'
Variable names:
  ​0: closure
Cell variables:
  ​0: x

挖掘闭包本身
>>> dis.show_code(closure_test())  # call outer
Name:              closure
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        2
Flags:             OPTIMIZED, NEWLOCALS, NESTED
Constants:
  ​0: None1: 2
Names:
  ​0: print
Free variables:
  ​0: x
>>> dis.show_code(lambda: print(2))
Name:              <lambda>
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        2
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
  ​0: None1: 2
Names:
  ​0: print

使用 Python 3.9.10

其他相关问题


1
谢谢!那真的很有帮助。 - akm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接