结论先行:闭包(closure)不是"函数套函数"的花活,而是------一个内层函数"记住了"它定义时所在外层作用域里的变量,哪怕外层函数早就执行完了返回了。记住的不是值,是那个变量本身(绑定)。一句话:闭包 = 内层函数 + 它捕获的外部变量,打包带走。
1. 先搞清:什么叫"捕获了外部变量"
普通函数里的局部变量,函数一返回就被销毁。但如果一个函数里定义了另一个函数,并且这个内层函数用到了外层的变量,Python 会把那些变量"封存"进内层函数的 __closure__ 里带走。
python
def make_counter():
count = 0 # 外层局部变量
def inc():
nonlocal count
count += 1
return count
return inc # 返回内层函数,count 被一起带走
c = make_counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3
关键点 :count 没有随 make_counter 返回而消失------它被 inc 捕获了,活在了 c 这个闭包里。你每调一次 c(),操作的都是同一个被捕获的 count。
2. 怎么验证"带走的是变量,不是值"
看闭包对象自带的证据:
python
print(c.__closure__) # (<cell at ...: int object>,)
print(c.__closure__[0].cell_contents) # 3 ← 当前捕获值
为什么重要:捕获的是变量绑定,所以后续修改对闭包"可见"。这正是计数器能累加的原因。若你以为是"传值",就解释不了为什么三次调用共享状态。
3. 最经典的坑:循环里闭包捕获同一个变量
python
funcs = []
for i in range(3):
def f():
return i # 捕获的是 i 这个变量,不是 0/1/2
funcs.append(f)
print([f() for f in funcs]) # [2, 2, 2] ← 全都是循环结束后的 i=2
为什么翻车 :三个 f 捕获的是同一个 i,循环跑完 i=2,于是全返回 2。这不是 bug,是闭包语义本身。修法两种:
python
# 法一:用默认参数把当时的值"钉死"
for i in range(3):
def f(i=i):
return i
funcs.append(f)
# 法二:用 functools.partial / lambda 绑定
from functools import partial
4. 闭包 vs 类 vs 全局变量(到底该用谁)
| 维度 | 闭包 | 类 | 全局变量 |
|---|---|---|---|
| 状态保存在 | __closure__ 单元格 |
实例 __dict__ |
模块命名空间 |
| 多个独立状态 | 每个闭包一份 | 每个实例一份 | 全程序共享一份 |
| 可读性 | 轻量但隐晦 | 显式清晰 | 易污染 |
| 适用 | 小型工厂/回调/装饰器 | 状态复杂、要方法 | 几乎别用 |
对照结论:要的是"带状态的小函数"→ 闭包;状态多、要方法 → 用类;全局变量能不用就不用(并发下炸得最隐蔽)。
5. 一个反直觉点:闭包捕获的是"变量",延迟绑定要小心
闭包里的变量是延迟求值的------只有真正调用时才去读当前值。上面第 3 节的循环坑,本质就是延迟绑定 + 共享变量。记住口诀:循环里造闭包,要么默认参数钉值,要么 partial 绑定,别裸捕获循环变量。
对照表:一眼分清
| 场景 | 正确写法 | 错误写法 |
|---|---|---|
| 计数器/累加器 | nonlocal + 闭包 或 类 |
裸捕获外层变量 |
| 循环生成多个函数 | 默认参数 i=i 或 partial |
裸 return i |
| 只读配置 | 闭包捕获常量 | 全局变量 |
排错清单(收藏级)
SyntaxError: no binding for nonlocal:nonlocal声明的变量外层必须真存在,且不能是全球量;漏写nonlocal count直接报。- 循环闭包全返回同一个值 :延迟绑定 + 共享变量(见第 3 节),用默认参数或
partial钉值。 - 闭包修改捕获值报错 :忘了
nonlocal/global,Python 会当成新建局部变量 → 加nonlocal。 - 装饰器里的闭包不生效 :装饰器本质是闭包工厂,确认内层真的引用了外层参数;
@wraps别忘了保留元信息。 - 内存泄漏担忧:闭包持有外部变量,只要闭包对象活着,那些变量就不释放;长生命周期闭包别捕获大对象。
一句话记住 :闭包就是"内层函数把外层变量打包带走"------带走的是变量绑定不是值,循环里造闭包记得钉值,要改捕获值就 nonlocal。