进阶篇 · Advanced
lv.3 进阶
kp-023
闭包与作用域
1. 一句话定义
闭包 = 内层函数 + 它捕获的外层作用域变量;只要内层函数活着,被捕获的变量就活着——函数成了"带状态的对象"。
2. 为什么重要
闭包是装饰器(kp-024)、回调、工厂函数的公共底座;也是理解"函数为什么能记住东西"的机制层答案。面试与源码阅读双高频。
3. 前置知识
kp-007(LEGB、nonlocal)。
4. 核心概念
- 捕获的是变量不是值:闭包记住的是外层变量的"格子",外层改了它跟着变。
cell对象:被内层引用的外层变量存放在 cell 里,__closure__可内省。- late binding:循环里创建的多个闭包共享同一个循环变量——著名陷阱。
5. 原理与机制
python
def make_counter():
n = 0 # 被 step 引用 → 进入 cell
def step():
nonlocal n # 允许写外层变量
n += 1
return n
return step
c1, c2 = make_counter(), make_counter()
c1(); c1() # 1, 2 —— 各自独立的状态
c2() # 1 —— 不与 c1 共享
# late binding 陷阱
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] !都引用同一个 i(最终值)
# 修复:用默认参数在定义时求值
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]验证捕获:step.__closure__ 是 cell 元组,cell.cell_contents 取当前值。
6. 关键事实(模型/图示)
text
make_counter() 调用结束后:
函数帧本该销毁 ──但 step.__closure__ 还引用着 n 的 cell──► n 活着
每次调用 make_counter 产生新的 n 与新的闭包(状态彼此独立)7. 直观类比
闭包像便签本被带走:员工(内层函数)离职(外层函数返回)时,把他负责的那页便签(被捕获变量)撕下来带走了——别人撕不回去,但他自己随时能翻看和续写。每个员工带走自己那页,互不串页。
8. 实例与案例
python
# 工厂函数:按配置生成行为
def make_throttle(limit_per_sec):
last = [0.0]
def allow(now):
if now - last[0] >= 1 / limit_per_sec:
last[0] = now
return True
return False
return allow(用单元素列表而非 nonlocal 是因为旧代码习惯;现代写法用 nonlocal 更清晰。)
9. 常见误区
- 以为闭包复制了值 —— 捕获变量本身;外层后续修改对内层可见。
- 循环变量 late binding(上文)—— 记住"lambda 捕获的是名字不是值",修复用默认参数或
functools.partial。 - 在内层只读不写时也写 nonlocal —— 只读不需要声明;nonlocal 只为赋值服务。
- 把闭包状态当持久化 —— 进程结束即消失;需要持久化用文件/DB(kp-016)。
10. 自测题
funcs = [lambda: i for i in range(3)]为什么三次调用都返回 2?__closure__是什么?什么时候为 None?- 闭包与"用类实现计数器"的取舍?
参考答案
- lambda 捕获的是循环变量 i 的绑定;推导式结束后 i 停留在 2,所有 lambda 共享。
- 被捕获变量的 cell 元组;内层函数没有引用任何外层变量时为 None。
- 状态少、行为单一用闭包更轻;状态多、需要多方法/继承/可测试注入时用类。
11. 与其他知识点的关系
12. 延伸阅读
- Fluent Python 第 7 章(函数作为一等对象)
- PEP 227(Statically Nested Scopes)