39. 闭包和单元格
39. 闭包和单元格
闭包允许嵌套函数在封闭函数返回后使用该封闭函数中的变量。```python def make_adder(n): def add(x): return x + n return add
add10 = make_adder(10)
print(add10(5))
输出:text
15
```变量n属于make_adder, 但add以后还是用它。 CPython 通过将捕获的变量移动到单元对象中来支持这一点。内部函数保留对这些单元格的引用。
39.1 嵌套函数
嵌套函数是在另一个函数内部定义的函数。```python def outer(): def inner(): return 1
return inner
```这def inner语句执行时outer运行。它创建一个函数对象并将其绑定到本地名称inner。
呼唤outer()返回该函数对象:python fn = outer() print(fn()) 函数对象为inner包含:text code object globals dictionary defaults keyword defaults annotations closure cells, if needed 如果inner不引用外部局部变量,它不需要闭包。
39.2 自由变量
自由变量是由代码对象使用但在封闭范围内定义的变量。```python def outer(): x = 10
def inner():
return x
return inner
```里面inner, x是一个自由变量。
您可以检查一下:```python def outer(): x = 10
def inner():
return x
return inner
fn = outer()
print(fn.code.co_freevars)
输出:text
('x',)
```内部代码对象记录了它需要的x从外面。
39.3 单元格变量
单元变量是必须由内部函数捕获的局部变量。```python def outer(): x = 10
def inner():
return x
return inner
```为了outer, x是一个单元变量,因为内部函数使用它。
检查:python print(outer.__code__.co_cellvars) 输出:text ('x',) 从两个方向看同一个变量:```text
outer:
x is a cell variable
inner: x is a free variable
普通的局部变量位于帧槽中。```python
def f():
x = 10
return x
```后`f`返回时,其框架可以被破坏。它的本地插槽消失了。
但这不适用于闭包:```python
def outer():
x = 10
def inner():
return x
return inner
```后`outer`返回,`inner`还需要`x`。
CPython 通过存储来解决这个问题`x`在单元格对象中。
从概念上讲:```text
outer frame
x slot points to cell
cell contains 10
inner function
closure tuple points to same cell
```什么时候`outer`返回时,框架可以消失,但单元格仍然活着,因为`inner`引用它。
## 39.5 单元格对象
单元对象是一个小容器,其中保存对另一个 Python 对象的引用。
从概念上讲:```text
cell
contents -> object
```为了:```python
def outer():
x = 10
def inner():
return x
return inner
```闭包看起来像:```text
inner function
__closure__:
cell for x
contents: 10
```检查它:```python
fn = outer()
print(fn.__closure__)
print(fn.__closure__[0].cell_contents)
```输出形状:```text
(<cell at 0x...: int object at 0x...>,)
10
```幸存下来的是细胞,而不是整个外部框架。
## 39.6 函数闭包元组
一个函数对象有一个`__closure__`属性。```python
def outer():
x = 10
def inner():
return x
return inner
fn = outer()
print(fn.__closure__)
__closure__是None或单元格对象的元组。
对于这个例子:text fn.__closure__ -> (cell_for_x,) 单元格的顺序对应于fn.__code__.co_freevars。python print(fn.__code__.co_freevars) for cell in fn.__closure__: print(cell.cell_contents) 输出:```text
('x',)
10
使用这个例子:```python
def outer(a):
b = 2
def inner(c):
return a + b + c
return inner
fn = outer(10)
print(outer.__code__.co_cellvars)
print(fn.__code__.co_freevars)
print(fn.__closure__)
```输出形状:```text
('a', 'b')
('a', 'b')
(<cell ...>, <cell ...>)
```外部函数具有单元变量`a`和`b`。
内部函数有自由变量`a`和`b`。
返回的函数携带两者的单元格。
## 39.8`LOAD_DEREF`闭包变量通过取消引用字节码来访问。
为了:```python
def outer():
x = 10
def inner():
return x
return inner
inner不使用LOAD_FAST为了x。它使用闭包访问。
从概念上讲:```text LOAD_DEREF x RETURN_VALUE
`LOAD_DEREF`读取单元格的内容。
类似地,存储到闭包变量中使用面向解引用的存储指令。
## 39.9 拆卸闭包
使用`dis`:
```python
import dis
def outer():
x = 10
def inner():
return x
return inner
dis.dis(outer)
fn = outer()
dis.dis(fn)
```寻找:```text
MAKE_CELL
LOAD_CLOSURE
MAKE_FUNCTION
LOAD_DEREF
STORE_DEREF
```确切的指令因 CPython 版本而异,但概念操作是稳定的:```text
create cell for captured local
create inner function with closure
load from closure cell inside inner
```## 39.10 闭包创建
当 CPython 为嵌套函数创建函数对象时,如果嵌套函数需要自由变量,它会附加闭包单元。
从概念上讲:```text
outer executes
create cell for x
load inner code object
load closure cell for x
make function object with closure
return function object
```为了:```python
def outer():
x = 10
def inner():
return x
return inner
```返回的函数有:```text
inner.__code__
inner.__globals__
inner.__closure__ = (cell_for_x,)
```该闭包元组是如何`inner`看到`x`。
## 39.11 闭包捕获变量,而不是值
闭包通过单元捕获变量,而不是值的快照。```python
def outer():
x = 1
def inner():
return x
x = 2
return inner
fn = outer()
print(fn())
```输出:```text
2
```内部函数查看单元格的当前内容。单元格之前已更新`outer`回来了。
从概念上讲:```text
cell x initially contains 1
cell x later contains 2
inner reads cell x
```## 39.12 共享单元
多个内部函数可以共享同一个单元格。```python
def outer():
x = 0
def get():
return x
def set_value(v):
nonlocal x
x = v
return get, set_value
get, set_value = outer()
print(get())
set_value(10)
print(get())
```输出:```text
0
10
```两个函数引用相同的单元格`x`。```text
get.__closure__[0] -> cell x
set_value.__closure__[0] -> same cell x
```单元格提供共享的可变绑定。
## 39.13`nonlocal`
`nonlocal`告诉编译器赋值应该针对一个封闭的函数变量,而不是创建一个新的局部变量。```python
def outer():
x = 0
def inc():
nonlocal x
x += 1
return x
return inc
```没有`nonlocal`, 赋值使`x`本地到`inc`:
```python
def outer():
x = 0
def bad():
x += 1
return x
return bad
```呼唤`bad()`提高`UnboundLocalError`因为`x += 1`尝试阅读本地`x`在它具有价值之前。
和`nonlocal`,CPython 对外部单元发出取消引用操作。
## 39.14`global`与`nonlocal`
`global`目标是模块名称空间。```python
x = 0
def f():
global x
x = 10
nonlocal目标是一个封闭的函数作用域。```python
def outer():
x = 0
def inner():
nonlocal x
x = 10
|声明|目标|
|---|---|
|`global x`|模块全局词典 |
|`nonlocal x`|最近的封闭函数范围`x`|
|没有带有赋值的声明 |当前本地范围 |`nonlocal`无法定位全局模块。它需要一个封闭的函数绑定。
## 39.15 结束和范围分析
编译器在符号表分析期间确定闭包布局。
对于每个代码块,它将名称分类为:```text
local
global explicit
global implicit
free
cell
```例子:```python
def outer():
x = 1
def inner():
return x
```分类:```text
outer:
x = local, promoted to cell
inner = local
inner:
x = free
```此分类决定发出哪些字节码指令。```text
local variable -> LOAD_FAST / STORE_FAST
global name -> LOAD_GLOBAL / STORE_GLOBAL
closure variable -> LOAD_DEREF / STORE_DEREF
```## 39.16 闭包生命周期
只要有东西引用它,细胞就会存在。
共同业主:```text
function closure tuple
active frame
generator frame
coroutine frame
another cell or object graph
```例子:```python
def outer():
x = [1, 2, 3]
def inner():
return x
return inner
fn = outer()
```该列表仍然有效:```text
fn
__closure__
cell
list [1, 2, 3]
```什么时候`fn`变得不可到达,闭包元组和单元可以被释放,并且如果不存在其他引用则可以释放列表。
## 39.17 闭包通常不会使整个框架保持活动状态
一个常见的误解是闭包使整个外部框架保持活动状态。
通常情况下不会。```python
def outer():
a = "captured"
b = "not captured"
def inner():
return a
return inner
```返回的函数需要`a`,但不是`b`。
从概念上讲:```text
inner keeps cell for a
inner does not keep b
outer frame can be destroyed
```这比保留整个框架更有效。
然而,如果一个框架对象本身通过内省被捕获,那么它可以让所有本地对象保持活力。
## 39.18 循环中的后期绑定
闭包捕获变量,而不是每次迭代的值。```python
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs])
```输出:```text
[2, 2, 2]
```所有 lambda 表达式都关闭同一变量`i`。循环结束后,`i`是`2`。
这是后期绑定:在函数运行时查找值,而不是在创建函数时查找。
## 39.19 捕获带默认值的当前值
使用默认参数来捕获当前值。```python
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
print([f() for f in funcs])
```输出:```text
[0, 1, 2]
```这里,每个 lambda 都有自己的默认参数值。
这使用函数默认值,而不是闭包单元。
从概念上讲:```text
lambda i=0: i
lambda i=1: i
lambda i=2: i
```## 39.20 推导式中的闭包
推导式有自己的作用域,但其中的闭包仍然可以显示后期绑定。```python
funcs = [lambda: x for x in range(3)]
print([f() for f in funcs])
```输出:```text
[2, 2, 2]
```循环变量`x`属于理解范围,并且所有 lambda 都封闭在同一个单元格内。
使用默认值捕获每次迭代的值:```python
funcs = [lambda x=x: x for x in range(3)]
print([f() for f in funcs])
```输出:```text
[0, 1, 2]
```## 39.21 闭包和可变性
闭包可以引用可变对象。```python
def outer():
xs = []
def add(x):
xs.append(x)
return xs
return add
add = outer()
print(add(1))
print(add(2))
```输出:```text
[1]
[1, 2]
```不`nonlocal`是必需的,因为该函数会改变列表对象。它不会重新绑定名称`xs`。
这有效:```python
xs.append(x)
```这需要`nonlocal`:
```python
xs = xs + [x]
```第二种形式重新分配名称。
## 39.22 重新绑定与变异
比较:```python
def outer():
xs = []
def add(x):
xs.append(x)
return add
```和:```python
def outer():
xs = []
def add(x):
xs = xs + [x]
return add
```第一个改变引用的对象`xs`。
第二个分配给`xs`,所以编译器会处理`xs`作为本地`add`除非声明`nonlocal`。
正确的重新绑定:```python
def outer():
xs = []
def add(x):
nonlocal xs
xs = xs + [x]
return xs
return add
```## 39.23 闭包和函数工厂
闭包通常用于函数工厂。```python
def power(exp):
def apply(x):
return x ** exp
return apply
square = power(2)
cube = power(3)
print(square(5))
print(cube(5))
```输出:```text
25
125
```每次致电`power`创建一个新单元格`exp`。```text
square closure:
exp = 2
cube closure:
exp = 3
```代码对象为`apply`可以共享,但闭合单元不同。
## 39.24 闭包和装饰器
装饰器通常使用闭包。```python
def log_calls(fn):
def wrapper(*args, **kwargs):
print("calling", fn.__name__)
return fn(*args, **kwargs)
return wrapper
```使用:```python
@log_calls
def add(a, b):
return a + b
```从概念上讲:```python
def add(a, b):
return a + b
add = log_calls(add)
```返回的`wrapper`关闭原来的`fn`。```text
wrapper.__closure__
cell for fn -> original add function
```## 39.25 闭包和状态
闭包可以存储私有状态。```python
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
c = counter()
print(c())
print(c())
```输出:```text
1
2
```国家`n`不存储在实例上。它被存储在封闭单元中。
这类似于具有一种方法和私有状态的小对象。
## 39.26 闭包状态与对象状态
关闭版本:```python
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
```对象版本:```python
class Counter:
def __init__(self):
self.n = 0
def inc(self):
self.n += 1
return self.n
```比较:
|特色|关闭 |对象|
|---|---|---|
|状态存储|单元格变量 |实例属性 |
|多重操作 |不太方便 |自然 |
|内省|不太明确 |更明确 |
|小回调状态 |身材好|身材好|
|丰富的行为|尴尬|更好 |
闭包最适合小型捕获状态。类更适合较大的协议。
## 39.27 闭包和引用循环
闭包可以参与循环。```python
def outer():
funcs = []
def inner():
return funcs
funcs.append(inner)
return inner
fn = outer()
```参考图:```text
inner function
closure cell
funcs list
inner function
```当无法到达时,这个循环可以被 CPython 的循环垃圾收集器收集。
涉及终结器或外部资源的循环需要更加小心。
## 39.28 闭包和内存保留
闭包可以保留大的物体。```python
def make_reader():
data = bytearray(100_000_000)
def read():
return data[0]
return read
reader = make_reader()
```大的`data`只要对象保持活动状态`reader`做。
保留链:```text
reader function
closure tuple
cell
large bytearray
```如果闭包不再需要大对象,请清除或避免捕获它。
## 39.29 避免意外捕获
这捕获了`self`:
```python
class C:
def make_callback(self):
def callback():
return self.value
return callback
```返回的回调使实例保持活动状态。
有时这是有意的。有时它会产生意想不到的保留。
您可以仅捕获所需的值:```python
class C:
def make_callback(self):
value = self.value
def callback():
return value
return callback
```现在回调保持`value`,不一定是整个实例。
## 39.30 检查关闭内容
使用`__closure__`小心:```python
def outer():
x = {"a": 1}
def inner():
return x
return inner
fn = outer()
for name, cell in zip(fn.__code__.co_freevars, fn.__closure__):
print(name, cell.cell_contents)
```输出:```text
x {'a': 1}
```这对于学习和调试很有用,但生产代码应该很少依赖于闭包内部。
## 39.31 空单元格
在某些边缘情况下,闭合单元可能为空。
涉及删除的示例模式可能会产生空单元格:```python
def outer():
x = 1
def inner():
return x
del x
return inner
```呼叫:```python
fn = outer()
fn()
```会引发错误,因为单元格不再包含内容。
访问`cell.cell_contents`对于空单元格会引发`ValueError`。
## 39.32 关闭和`del`删除非局部变量会清除单元格。```python
def outer():
x = 1
def delete():
nonlocal x
del x
def read:
return x
return read, delete
```更正版本:```python
def outer():
x = 1
def delete():
nonlocal x
del x
def read():
return x
return read, delete
```使用:```python
read, delete = outer()
print(read())
delete()
print(read())
```决赛`read()`由于单元格为空而引发。
## 39.33 关闭和默认不同
默认值存储在函数对象上。```python
def f(x=[]):
return x
```闭合单元存储在`__closure__`。```python
def outer():
x = []
def inner():
return x
return inner
```检查:```python
print(f.__defaults__)
print(outer().__closure__)
```他们解决不同的问题。
|机制|商店 |
|---|---|
|默认参数 |省略参数时使用的值 |
|闭合单元|来自封闭范围的变量 |
后期绑定的默认参数技巧之所以有效,是因为默认值是在函数创建时评估的。
## 39.34 闭包和 Lambda 表达式`lambda`函数遵循与以下相同的闭包规则`def`。```python
def outer():
x = 10
return lambda y: x + y
fn = outer()
print(fn(5))
```输出:```text
15
```lambda 有一个自由变量`x`。```python
print(fn.__code__.co_freevars)
print(fn.__closure__[0].cell_contents)
```lambda 只是一个紧凑的函数表达式。它没有特殊的闭合模型。
## 39.35 闭包和生成器
生成器可以关闭变量。```python
def outer(limit):
def gen():
for i in range(limit):
yield i
return gen
make = outer(3)
print(list(make()))
```生成器函数结束`limit`。
什么时候`make()`被调用时,它创建一个生成器对象。该生成器对象可以访问闭包单元。
有两层:```text
generator function
closure cell for limit
generator object
suspended frame when running
references generator function/code/closure state
```## 39.36 闭包和协程
异步函数也可以关闭变量。```python
def outer(delay):
async def wait_then_return(value):
await sleep(delay)
return value
return wait_then_return
```异步函数结束`delay`。
调用它会创建一个协程对象。挂起时,协程保持其框架状态,并且函数携带闭包单元。
这可以在等待过程中保留捕获的对象。
## 39.37 关闭性能
访问闭包变量通常比访问快速局部变量慢。
快速本地:```text
LOAD_FAST
```闭包变量:```text
LOAD_DEREF
LOAD_DEREF必须通读一个单元格。
在大多数代码中,这个成本很小。在非常热的循环中,本地绑定可能很重要。
例子:python def outer(value): def inner(xs): v = value total = 0 for x in xs: total += x + v return total return inner 这里,v是一个快速本地里面inner, 尽管value从单元格中读取一次。
39.38 关闭调试
调试关闭行为时,请检查:python fn.__code__.co_freevars fn.__closure__ cell.cell_contents 例子:```python
def outer():
x = 1
y = 2
def inner():
return x + y
return inner
fn = outer()
for name, cell in zip(fn.code.co_freevars, fn.closure): print(name, cell.cell_contents)
## 39.39 常见误解
|误会 |正确型号 |
|---|---|
|闭包复制值 |他们通过细胞捕捉变量|
|闭合使整个外框保持活力 |通常它只保留需要的细胞 |
|`nonlocal`意味着全球 |它的目标是一个封闭的函数作用域 |
|改变捕获的列表需求`nonlocal`|只需重新绑定名称即可`nonlocal`|
| Lambda 具有不同的闭包规则 |拉姆达和`def`使用相同的闭合模型|
|循环闭包捕获每个迭代值 |他们捕获循环变量 |
|默认和关闭是相同的 |默认存储值;封闭储存细胞|
|闭包变量是快速局部变量 |它们是通过细胞访问的 |
## 39.40 阅读策略
从以下开始:```python
def outer():
x = 10
def inner():
return x
return inner
```检查:```python
import dis
fn = outer()
print(outer.__code__.co_cellvars)
print(fn.__code__.co_freevars)
print(fn.__closure__)
print(fn.__closure__[0].cell_contents)
dis.dis(outer)
dis.dis(fn)
```然后测试:```text
nonlocal rebinding
multiple inner functions sharing one variable
lambda inside loops
default-argument capture
closures holding large objects
closures inside generators
closures inside async functions
```对于每种情况,跟踪:```text
which scope binds the name
whether the name becomes a cell variable
which inner function sees it as a free variable
which function owns the closure tuple
how long the cell remains alive
```## 39.41 章节总结
闭包是 CPython 的机制,用于让嵌套函数访问封闭函数作用域中的变量。当内部函数使用外部局部变量时,CPython 将该变量存储在单元格中。内部函数带有一个包含所需单元格的闭包元组。
核心模型是:```text
outer function local used by inner function
↓
local becomes a cell variable
↓
inner function records it as a free variable
↓
function object stores closure tuple
↓
cell remains alive after outer frame returns
↓
inner function reads or writes cell contents
```闭包解释了嵌套函数、装饰器、函数工厂、回调状态、`nonlocal`、循环中的后期绑定以及通过捕获的变量保留内存。