// 编程,精准讲解
在AI时代精通编程
越来越多的代码由AI写出,你仍然要读懂它、审查它,并准确说出自己需要什么。这里的每个主题都能在浏览器中运行,并以“生成代码该检查什么”收尾。
每日一练 · 9月23日
审查生成的Node镜像
17行代码里藏着5处问题。在上线之前把它们找出来。
# syntax=docker/dockerfile:1
FROM node:latest AS build
WORKDIR /app
ARG NPM_TOKEN
COPY package*.json ./
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc \ 语言版本
从一个方向开始
全部22个方向 → 01 浏览器内运行 JavaScript、TypeScript、Python和SQL均可直接运行。编辑示例,点击“运行”,立即查看输出。代码不会离开你的设备。 02 深度可调 “速览”先给出TL;DR和一个示例;“标准”补充原理和易踩的坑;“深入”再讲内部机制与边界情况。 03 一篇文章,两种语言 每个主题都有中文和英文版本,可随时切换语言,按自己的习惯阅读。 04 真正记得住的练习 根据阅读内容设计输出预测题、找错题、阶段测验和间隔复习闪卡。 05 你的AI也能读 每个页面都有对应的Markdown和llms.txt索引。一键在Claude或ChatGPT中打开相关章节,并附上引导学习的提示词。 06 版本与参考资料 每个主题注明适用的语言版本并提供参考资料,方便你结合自己使用的工具查阅。
问问你的AI
带上页面上下文的提示词
示例主题:闭包
假设我只懂循环,请讲解这一节 Claude ChatGPT I am reading "闭包" on CodeWiki (https://codewiki.com/zh/python/closures/), section "是什么,为什么存在".
Context (verbatim from the page):
"""
## 是什么,为什么存在
闭包是一个函数对象,以及它对定义位置中所需外层绑定的访问能力。在Python中,最常见的形式是外层函数创建并返回一个内层函数。外层调用已经返回,内层函数仍可使用那次调用留下的绑定。
内层函数使用、但不在自身代码块中绑定的名称叫作自由变量(free variable)。提供该绑定的词法作用域叫作外层作用域(enclosing scope)。函数的代码与这些绑定合在一起,才有调用时所需的完整上下文。
闭包让函数在作为值传递时仍遵守词法作用域。它适合把少量配置附在回调上,或者通过一个很窄的函数接口保存状态,而无需模块全局变量。函数工厂、装饰器、验证器、事件处理器,以及传给`sorted()`的键函数中都能见到它。
返回内层函数不是形成闭包的语义条件,只是让闭包的生命周期最容易观察。只要嵌套函数引用了外层函数的绑定,它在外层调用期间执行时也仍是闭包。反过来,嵌套函数若只使用参数、局部名称和全局名称,`__closure__`就是`None`。
闭包适合一种主要操作和少量容易命名的状态。如果调用方需要多个公开操作、验证规则、可序列化表示或继承关系,类通常能更直接地表达契约。选择依据是接口与所有权,而不是笼统的速度或内存说法。
"""
Explain this as if I only know loops. Use one small example and no term you have not explained first.
Answer in 中文. Keep code examples in Python. Where you are unsure, say so.
在这个示例里设置三个bug,让我来找 Claude ChatGPT 内容较长:链接只带开头部分和原文地址,打开时会同时复制完整提示词。 I am reading "闭包" on CodeWiki (https://codewiki.com/zh/python/closures/), section "示例".
Context (verbatim from the page):
"""
## 示例
下面四个示例从只读配置开始,再加入可变状态、共享操作和循环回调。所有输出都由本地`python3`实际执行对应文件得到。
### 携带只读配置
标签工厂把前缀配置一次,返回只接收订单编号的函数。`prefix`只被读取,因此不需要`nonlocal`。
<!-- quick -->
```python
# file: label_factory.py
def make_labeler(prefix):
def label(order_id):
return f"{prefix}-{order_id:04d}"
return label
invoice_label = make_labeler("INV")
return_label = make_labeler("RET")
print(invoice_label(7))
print(return_label(7))
print(invoice_label.__code__.co_freevars)
print(invoice_label.__closure__[0].cell_contents)
```
```text
INV-0007
RET-0007
('prefix',)
INV
```
<!-- /quick -->
两次`make_labeler()`调用各自创建一个`prefix`绑定,所以两个标签函数互不影响。`co_freevars`给出自由变量名称,`__closure__`中相同位置的cell保存对应绑定;这种按位置检查适合诊断,不适合作为业务接口。
字符串对象恰好不可变,但这不是闭包复制了字符串。函数仍通过自己的绑定访问`"INV"`。如果外层代码后来通过另一个闭包重新绑定同一个名称,读取函数就会看到新对象。
### 用`nonlocal`保存状态
计数器需要让`count`指向一个新整数,所以`record()`明确声明`nonlocal count`。每次工厂调用都产生独立计数。
```python
# file: attempt_counter.py
def make_attempt_counter(start=0):
count = start
def record():
nonlocal count
count += 1
return count
return record
email_attempt = make_attempt_counter()
sms_attempt = make_attempt_counter(10)
print(f"email: {email_attempt()}, {email_attempt()}")
print(f"sms: {sms_attempt()}")
print(f"email: {email_attempt()}")
```
```text
email: 1, 2
sms: 11
email: 3
```
交错调用表明两个计数器没有泄漏状态。把`email_attempt`赋给另一个名称不会创建新计数器,因为两个名称仍指向同一个函数对象;需要独立状态时,必须再次调用工厂。
这个接口只允许调用方推进计数,不能直接把`count`设成任意值。它提供的是作用域形成的封装约定,不是安全边界;检查函数对象的代码仍能看到cell内容。
### 让多个操作共享一个cell
一次`make_quota()`调用返回两个函数。`reserve()`重新绑定`remaining`,`available()`读取它;两者连接到同一个cell。
```python
# file: shared_quota.py
def make_quota(limit):
remaining = limit
def reserve(units):
nonlocal remaining
if units <= 0:
raise ValueError("units must be positive")
if units > remaining:
return False
remaining -= units
return True
def available():
return remaining
return reserve, available
reserve, available = make_quota(5)
print(reserve(2), available())
print(reserve(4), available())
print(reserve(3), available())
```
```text
True 3
False 3
True 0
```
第二次预留失败后,`remaining`没有变化。两个函数共享状态使这个结果可以从`available()`观察到。若操作继续增多,返回一组匿名位置上的函数会变得难读,此时带有`reserve()`和`available()`方法的类通常更合适。
共享cell不提供同步。若多个线程或异步任务可以交错执行检查与扣减,`if units > remaining`和`remaining -= units`不是一个业务原子操作;需要根据执行模型增加锁或把状态交给单一所有者。
### 对比延迟绑定与创建时绑定
循环中的两个列表都生成三个lambda。第一组直接引用循环变量,第二组把当轮对象保存为默认参数。
```python
# file: loop_handlers.py
def make_handlers(queue_names):
late_handlers = []
bound_handlers = []
for queue_name in queue_names:
# 这一组函数共享循环变量的 cell。
late_handlers.append(lambda: queue_name)
# 默认参数在创建函数时保存当前对象。
bound_handlers.append(lambda queue_name=queue_name: queue_name)
return late_handlers, bound_handlers
late, bound = make_handlers(["fast", "bulk", "slow"])
print([handler() for handler in late])
print([handler() for handler in bound])
print(late[0].__closure__[0] is late[1].__closure__[0])
print(bound[0].__closure__)
```
```text
['slow', 'slow', 'slow']
['fast', 'bulk', 'slow']
True
None
```
第一组函数在循环结束后才读取`queue_name`,此时共享cell指向`"slow"`。这就是延迟绑定(late binding)陷阱。列表推导式有自己的作用域,但在同一次推导中创建并稍后调用的闭包仍会共享那个推导变量。
第二组的左侧`queue_name`是lambda的局部形参,右侧名称在执行lambda定义时求值。保存后的对象位于函数默认值中,而不是闭包cell中,所以`__closure__`是`None`。如果调用方不应覆盖这个参数,调用一次辅助工厂通常比暴露默认参数更清楚。
"""
Introduce three subtle bugs into the example above and let me find them. Reveal each answer only after I have guessed.
Answer in 中文. Keep code examples in Python. Where you are unsure, say so.
根据本页内容考我,直到我答对五题 Claude ChatGPT 内容较长:链接只带开头部分和原文地址,打开时会同时复制完整提示词。 I am reading "闭包" on CodeWiki (https://codewiki.com/zh/python/closures/).
Context (verbatim from the page):
"""
# 闭包
Source: https://codewiki.com/zh/python/closures/
> - **what**: 闭包(closure)是关联了外层词法绑定的函数;即使定义它的调用已经结束,函数仍能读取或更新这些绑定。
> - **trap**: 闭包保留的是变量绑定,不是创建函数时冻结的值,因此循环中生成的回调可能全都读到最后一次迭代的值。
> - **fix**: 重新绑定外层状态时使用`nonlocal`;每个回调需要独立值或状态时,在创建时绑定参数,或者为每次迭代调用一次工厂。
## 是什么,为什么存在
闭包是一个函数对象,以及它对定义位置中所需外层绑定的访问能力。在Python中,最常见的形式是外层函数创建并返回一个内层函数。外层调用已经返回,内层函数仍可使用那次调用留下的绑定。
内层函数使用、但不在自身代码块中绑定的名称叫作自由变量(free variable)。提供该绑定的词法作用域叫作外层作用域(enclosing scope)。函数的代码与这些绑定合在一起,才有调用时所需的完整上下文。
闭包让函数在作为值传递时仍遵守词法作用域。它适合把少量配置附在回调上,或者通过一个很窄的函数接口保存状态,而无需模块全局变量。函数工厂、装饰器、验证器、事件处理器,以及传给`sorted()`的键函数中都能见到它。
返回内层函数不是形成闭包的语义条件,只是让闭包的生命周期最容易观察。只要嵌套函数引用了外层函数的绑定,它在外层调用期间执行时也仍是闭包。反过来,嵌套函数若只使用参数、局部名称和全局名称,`__closure__`就是`None`。
闭包适合一种主要操作和少量容易命名的状态。如果调用方需要多个公开操作、验证规则、可序列化表示或继承关系,类通常能更直接地表达契约。选择依据是接口与所有权,而不是笼统的速度或内存说法。
## 工作原理
Python按源码中的嵌套关系解析名称,而不是查看谁调用了当前函数。编译器发现内层代码引用了外层函数绑定的名称时,会把该名称归类为自由变量,并安排共享存储。调用栈上另一个同名局部变量不会改变这种选择。
每次执行外层函数,都会创建属于本次调用的局部绑定。创建内层函数时,函数对象取得访问所需绑定的路径;返回函数不会把对象值复制成快照。后续调用读取的是绑定当时指向的对象,所以重新绑定会改变闭包之后看到的结果。
一次闭包创建和调用可以按下面的顺序理解:
1. 调用外层函数,为这次调用创建局部绑定。
2. 执行内层函数定义,识别并连接它实际使用的外层绑定。
3. 把内层函数传出外层调用,或者在外层调用中直接使用它。
4. 外层调用结束后,仍被闭包引用的绑定继续存活。
5. 调用闭包时,名称查找读取或更新同一个绑定。
闭包不会自动保留外层函数的整个局部命名空间。只有内层代码实际作为自由变量使用的绑定需要留下。不过,一个绑定可能指向很大的可变对象或对象图,所以“只捕获一个名称”并不等于保留的数据一定很少。
### 读取、修改与重新绑定
读取自由变量不需要声明。修改自由变量所指向的可变对象也不需要声明,因为绑定本身没有变化。只有内层函数要让名称指向另一个对象时,才需要`nonlocal`。
| 内层操作 | 发生的事 | 是否需要`nonlocal` |
| --- | --- | --- |
| `return count` | 读取外层绑定 | 否 |
| `items.append(value)` | 修改绑定所指向的列表 | 否 |
| `count += 1` | 读取后重新绑定名称 | 是 |
| `items = []` | 把名称重新绑定到新列表 | 是 |
`count += 1`容易让人误判。整数不能原地修改,因此该语句先读取旧整数,再计算新整数,最后给`count`赋值。只要函数体中存在这种赋值,`count`默认就被归类为该函数的局部名称;没有`nonlocal count`时,读取尚未赋值的局部名称会抛出`UnboundLocalError`。
### `nonlocal`的目标
`nonlocal name`指向最近一个已经绑定该名称的外层函数作用域。它不会创建外层绑定,也不能指向模块全局作用域。编译器找不到合适绑定时,会在代码执行前抛出`SyntaxError`。
`global`与`nonlocal`解决的不是同一个问题。`global`把赋值指向模块命名空间;把生成代码中的`nonlocal`机械替换为`global`,会把本应属于某次工厂调用的状态变成整个模块共享的状态。
同一次外层调用创建的多个函数可以共享绑定;不同外层调用通常取得不同绑定。因此,“函数来自同一个工厂”不表示它们共享状态,真正决定共享关系的是它们来自哪一次工厂调用,以及引用了哪个绑定。
## 示例
下面四个示例从只读配置开始,再加入可变状态、共享操作和循环回调。所有输出都由本地`python3`实际执行对应文件得到。
### 携带只读配置
标签工厂把前缀配置一次,返回只接收订单编号的函数。`prefix`只被读取,因此不需要`nonlocal`。
<!-- quick -->
```python
# file: label_factory.py
def make_labeler(prefix):
def label(order_id):
return f"{prefix}-{order_id:04d}"
return label
invoice_label = make_labeler("INV")
return_label = make_labeler("RET")
print(invoice_label(7))
print(return_label(7))
print(invoice_label.__code__.co_freevars)
print(invoice_label.__closure__[0].cell_contents)
```
```text
INV-0007
RET-0007
('prefix',)
INV
```
<!-- /quick -->
两次`make_labeler()`调用各自创建一个`prefix`绑定,所以两个标签函数互不影响。`co_freevars`给出自由变量名称,`__closure__`中相同位置的cell保存对应绑定;这种按位置检查适合诊断,不适合作为业务接口。
字符串对象恰好不可变,但这不是闭包复制了字符串。函数仍通过自己的绑定访问`"INV"`。如果外层代码后来通过另一个闭包重新绑定同一个名称,读取函数就会看到新对象。
### 用`nonlocal`保存状态
计数器需要让`count`指向一个新整数,所以`record()`明确声明`nonlocal count`。每次工厂调用都产生独立计数。
```python
# file: attempt_counter.py
def make_attempt_counter(start=0):
count = start
def record():
nonlocal count
count += 1
return count
return record
email_attempt = make_attempt_counter()
sms_attempt = make_attempt_counter(10)
print(f"email: {email_attempt()}, {email_attempt()}")
print(f"sms: {sms_attempt()}")
print(f"email: {email_attempt()}")
```
```text
email: 1, 2
sms: 11
email: 3
```
交错调用表明两个计数器没有泄漏状态。把`email_attempt`赋给另一个名称不会创建新计数器,因为两个名称仍指向同一个函数对象;需要独立状态时,必须再次调用工厂。
这个接口只允许调用方推进计数,不能直接把`count`设成任意值。它提供的是作用域形成的封装约定,不是安全边界;检查函数对象的代码仍能看到cell内容。
### 让多个操作共享一个cell
一次`make_quota()`调用返回两个函数。`reserve()`重新绑定`remaining`,`available()`读取它;两者连接到同一个cell。
```python
# file: shared_quota.py
def make_quota(limit):
remaining = limit
def reserve(units):
nonlocal remaining
if units <= 0:
raise ValueError("units must be positive")
if units > remaining:
return False
remaining -= units
return True
def available():
return remaining
return reserve, available
reserve, available = make_quota(5)
print(reserve(2), available())
print(reserve(4), available())
print(reserve(3), available())
```
```text
True 3
False 3
True 0
```
第二次预留失败后,`remaining`没有变化。两个函数共享状态使这个结果可以从`available()`观察到。若操作继续增多,返回一组匿名位置上的函数会变得难读,此时带有`reserve()`和`available()`方法的类通常更合适。
共享cell不提供同步。若多个线程或异步任务可以交错执行检查与扣减,`if units > remaining`和`remaining -= units`不是一个业务原子操作;需要根据执行模型增加锁或把状态交给单一所有者。
### 对比延迟绑定与创建时绑定
循环中的两个列表都生成三个lambda。第一组直接引用循环变量,第二组把当轮对象保存为默认参数。
```python
# file: loop_handlers.py
def make_handlers(queue_names):
late_handlers = []
bound_handlers = []
for queue_name in queue_names:
# 这一组函数共享循环变量的 cell。
late_handlers.append(lambda: queue_name)
# 默认参数在创建函数时保存当前对象。
bound_handlers.append(lambda queue_name=queue_name: queue_name)
return late_handlers, bound_handlers
late, bound = make_handlers(["fast", "bulk", "slow"])
print([handler() for handler in late])
print([handler() for handler in bound])
print(late[0].__closure__[0] is late[1].__closure__[0])
print(bound[0].__closure__)
```
```text
['slow', 'slow', 'slow']
['fast', 'bulk', 'slow']
True
None
```
第一组函数在循环结束后才读取`queue_name`,此时共享cell指向`"slow"`。这就是延迟绑定(late binding)陷阱。列表推导式有自己的作用域,但在同一次推导中创建并稍后调用的闭包仍会共享那个推导变量。
第二组的左侧`queue_name`是lambda的局部形参,右侧名称在执行lambda定义时求值。保存后的对象位于函数默认值中,而不是闭包cell中,所以`__closure__`是`None`。如果调用方不应覆盖这个参数,调用一次辅助工厂通常比暴露默认参数更清楚。
## 陷阱
### 把闭包说成值快照
> **陷阱:** “闭包保存了变量当时的值”会错误预测重新绑定和循环回调的结果。闭包通常保留对绑定的访问,而不是在函数创建时冻结对象。
**修复方法:** 画出每个自由名称对应的绑定,并标明函数创建与函数调用发生的时间。需要快照时,用默认参数、辅助工厂或显式复制表达这个决定,同时说明复制是浅层还是深层。
### 遗漏或误用`nonlocal`
> **陷阱:** 内层函数对名称赋值时,该名称默认在整个函数代码块中都是局部名称。即使赋值位于不会执行的分支,它仍会影响编译期分类。
**修复方法:** 确实要重新绑定外层函数状态时,靠近函数开头写出`nonlocal`。只读或原地修改可变对象时不要机械添加它;目标属于模块时才使用`global`。遇到`UnboundLocalError`,先检查函数中所有赋值位置,而不只是报错行。
### 错误放置工厂调用
> **陷阱:** 在循环外只调用一次有状态工厂,再把同一个结果注册给多个使用方,会让它们意外共享cell。相反,在每次处理事件时重新调用工厂,又会让本应持续的状态不断归零。
**修复方法:** 先写清状态属于应用、队列、请求还是单个回调,再把工厂调用放在对应生命周期的边界。测试时交错调用两个应该独立的实例,也连续调用一个应该保留状态的实例。
### 在循环中隐藏延迟绑定
> **陷阱:** 延迟绑定不只出现在`lambda`中。循环内的嵌套`def`、回调注册、任务完成处理器和推导式都可能让多个函数共享最后一个迭代绑定。
**修复方法:** 在循环结束后再调用所有生成函数,并断言每个结果。只需要固定一个值时可用默认参数;每个回调还需要独立可变状态时,应为每次迭代调用一次命名工厂。
### 捕获生命周期过长的对象
> **陷阱:** 长期注册的闭包会让它使用的绑定保持可达。若绑定指向请求上下文、缓存、服务容器或大型数据集,这些对象可能比实际工作存活更久。
**修复方法:** 提取回调真正需要的小型值,不要为了一个字段捕获整个上下文对象。所有者结束时解除注册;文件、套接字和事务等资源应由上下文管理器明确管理,而不是等待闭包被回收。
### 依赖`__closure__`的位置
> **陷阱:** `function.__closure__[0]`没有稳定的业务含义。增加另一个自由变量后,名称与cell的位置可能变化;没有自由变量时,`__closure__`还是`None`。
**修复方法:** 诊断时把`function.__code__.co_freevars`与`function.__closure__`按位置配对,或者使用`inspect.getclosurevars()`分类查看绑定。生产代码应通过公开函数接口读取或改变状态,不要把cell布局当作协议。
<!-- deep -->
## 绑定、函数对象与cell
语言层面最重要的保证是词法名称解析和外层绑定的持续可用性。CPython用cell对象(cell object)实现这种共享存储。其他Python实现必须保持可观察语义,但内部结构不必采用完全相同的字节码或对象布局。
### 编译期名称分类
编译器在函数运行前就确定名称属于局部、自由、cell还是全局类别。外层函数的局部名称若被内层函数引用,会在外层代码对象的`co_cellvars`中出现;对于内层代码对象,同一名称会出现在`co_freevars`中。
这种分类以整个代码块为单位,而不是沿执行路径临时决定。内层函数中任何赋值都会让目标默认为局部名称,除非`global`或`nonlocal`声明改变分类。因此,把赋值放进`if False:`也不会让更早的读取自动回到外层绑定。
| 观察位置 | 属性 | 表示的内容 |
| --- | --- | --- |
| 外层代码对象 | `co_cellvars` | 同时由内层代码引用的外层局部名称 |
| 内层代码对象 | `co_freevars` | 必须由外层作用域提供的名称 |
| 内层函数对象 | `__closure__` | 与`co_freevars`按位置对应的cell元组 |
| 诊断辅助函数 | `inspect.getclosurevars()` | 已解析的非局部、全局、内置与未绑定名称 |
代码对象描述的是编译结果,可以由多次工厂调用创建的函数对象共享。`__closure__`属于函数对象,连接的是某次运行产生的cell。不要把共享代码对象误认为共享运行时状态。
### cell的标识与共享关系
一个cell是保存当前对象引用的小型容器。对捕获名称执行`nonlocal`重新绑定时,变化的是cell中的引用;其他共享该cell的闭包会在下一次读取时看到新对象。对捕获列表执行`append()`时,cell仍指向原列表,但列表内容已经变化。
同一次外层调用可以创建多个共享cell的函数,这正是`reserve()`和`available()`看到同一配额的原因。再次调用工厂会创建新的外层绑定和新cell,所以新的配额与旧配额隔离。比较cell的对象标识可以用于测试这种关系,但不应成为业务控制流。
`__closure__`要么是`None`,要么是cell元组。读取`cell_contents`可以帮助定位泄漏状态;直接修改cell内容会把应用代码绑在实现细节上,还会绕过工厂本来提供的校验和不变量。
### 默认参数不是捕获cell
默认表达式在执行`def`或lambda表达式时求值,结果保存在函数的默认参数数据中。`lambda item=item: item`的左侧`item`是局部形参,因此函数体不需要从外层读取这个名称。这个技巧解决了时机问题,但它不改变Python的闭包规则。
默认参数也只保存对象引用,不会自动复制可变对象。如果循环变量指向之后仍会修改的字典,那么每个默认参数虽然各自保存当轮字典对象,调用时仍可能看到该字典的新内容。需要内容快照时,应在绑定前显式复制,并选择符合契约的复制深度。
`functools.partial()`可以预先绑定已有可调用对象的实参。它返回偏函数对象,不是通过自由变量保存这些实参的Python闭包。选择默认参数、辅助工厂或`partial()`时,应优先考虑调用签名和可读性,而不是把三者说成同一种机制。
### 多层嵌套与类作用域
多层函数嵌套中,`nonlocal`选择最近一个已有同名绑定的外层函数作用域。它不能跳过最近绑定去指定更远的同名绑定,也没有语法按层命名目标。出现多层同名状态时,改名往往比继续依赖隐式层级更清楚。
普通类命名空间不是方法体的外层函数作用域。在类体中写`rate = 0.2`,方法里直接使用`rate`不会像嵌套函数那样捕获它;方法应通过`self.rate`、`type(self).rate`或类名访问类属性。把类体和函数体都叫“外层”会掩盖这项差异。
定义在方法内部的函数可以捕获该方法的局部名称,包括`self`。这也意味着返回的回调可能让整个实例保持可达。只需要实例中的一个不可变字段时,先把字段值放入语义明确的局部名称,再决定是否捕获它。
### 诊断共享状态
怀疑闭包状态错误时,先从源码和调用生命周期入手,再检查运行时对象。下面这组检查通常足够定位问题:
1. 用`co_freevars`列出内层函数实际读取的自由名称。
2. 把这些名称与`__closure__`中的cell按位置配对。
3. 比较两个函数是否引用同一个cell,而不只比较`cell_contents`是否相等。
4. 用`inspect.getclosurevars()`区分非局部名称、全局名称、内置名称与未绑定名称。
相等的内容不能证明状态共享。两个独立计数器都可能从整数`0`开始,却拥有不同cell;相反,共享cell在不同时刻可以指向不同对象。所有权判断需要看cell标识和工厂调用边界。
### 生命周期与执行模型
闭包让捕获绑定至少活到最后一个引用它的函数不可达为止,但具体回收时间仍由Python实现和引用关系决定。不要让正确性依赖析构时机。必须按时释放的资源需要显式关闭、上下文管理器或明确的注销操作。
闭包状态没有特殊的并发保证。线程、任务、信号处理器或重入回调若能访问同一个闭包,检查后更新仍可能发生交错。解决方法与其他共享可变状态相同:缩小所有权、串行化访问,或者使用适合执行模型的同步原语。
标准`pickle`依靠模块中的限定名称定位普通函数,嵌套的局部函数通常无法按这种方式导入。需要把工作提交给进程池或跨进程传输时,不要假设闭包自然可序列化;顶层函数配合显式数据参数通常更可靠。
### 元数据与包装器行为
装饰器返回的闭包是新的函数对象。没有`functools.wraps()`时,它的`__name__`、文档、注解和`__wrapped__`链接描述的是包装器,而不是被包装函数。这会干扰回溯信息、API文档和检查签名的工具。
`wraps()`会复制选定元数据,并为检查工具暴露被包装对象;它不会改变包装器捕获了哪些值。它也不会隔离状态、增加同步或让局部包装器可序列化。这些仍是独立的设计决定。
类型注解可以描述返回的可调用对象签名,但普通Python执行不会强制注解。需要让装饰器在静态类型层面保留任意可调用签名时,可以使用`ParamSpec`;运行时闭包机制不会因此改变。
### 测试生命周期契约
大多数闭包测试应使用公开的可调用接口,而不是`__closure__`。执行一组调用,再断言返回值或可观察副作用。这样即使实现以后从闭包改成类,测试仍然有效。
延迟绑定测试必须把调用推迟到循环或注册阶段之后。在循环中立即调用会读到当轮值,可能让错误实现显得正确。迭代值还应彼此不同,避免重复的最终值被测试数据掩盖。
状态隔离测试至少需要两个工厂结果。交错调用它们,再确认各自的序列独立推进。对于刻意共享状态的一组操作,则应在每次修改后从两个接口观察结果。
### 闭包还是类
闭包在接口只有一种主要调用、状态很少且不需要公开检查时很顺手。它还能让配置后的函数直接交给要求可调用对象的API。名称良好的工厂与返回函数应说明状态属于谁,以及工厂需要调用多少次。
类适合多个操作、明确属性、协议实现、序列化和独立测试状态转换。把五六个闭包塞进字典或位置元组,往往已经在手工模拟对象接口。此时改用类不是否定闭包,而是让结构与公开契约一致。
<!-- /deep -->
[检查点: python/closures](https://codewiki.com/zh/python/closures/#checkpoint)
## 延伸阅读
- [Python语言参考:名称与绑定](https://docs.python.org/3.14/reference/executionmodel.html#naming-and-binding)
- [Python语言参考:`nonlocal`语句](https://docs.python.org/3.14/reference/simple_stmts.html#the-nonlocal-statement)
- [Python数据模型:用户定义函数](https://docs.python.org/3.14/reference/datamodel.html#user-defined-functions)
- [Python `inspect.getclosurevars()`](https://docs.python.org/3.14/library/inspect.html#inspect.getclosurevars)
- [Python编程常见问题:循环中的lambda](https://docs.python.org/3.14/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result)
- [PEP 3104:访问外层作用域中的名称](https://peps.python.org/pep-3104/)
"""
Quiz me on this page one question at a time. Wait for each answer and tell me what I got wrong, and keep going until I have answered five questions correctly.
Answer in 中文. Keep code examples in Python. Where you are unsure, say so.