02-闭包与作用域链
Python 版本要求:Python 3.11+
代码目录:
decorators_demo/app/decorators/ch02_closures.py测试验证:
uv run pytest tests/test_ch02.py -v
贯穿项目:Web API 请求处理系统 本节目标:理解 LEGB 查找规则、掌握闭包本质、学会用 nonlocal 修改外层变量
概念铺垫
为什么需要闭包?
问题场景:请求计数器
继续 Web API 系统,需要统计每个路由的请求次数:
# ❌ 方案一:全局变量(容易被篡改)
request_count: int = 0
def handle_request(req: Request) -> Response:
global request_count
request_count += 1
return Response(200, {"count": request_count})
request_count = 100 # 外部意外修改!
handle_request(req) # count = 101(错误)
# ✅ 方案二:闭包(私有变量,外部无法修改)
def make_counter(start: int = 0) -> Callable[[], int]:
count: int = start # 私有变量,外部无法访问
def increment() -> int:
nonlocal count
count += 1
return count
return increment
counter = make_counter(0)
print(counter()) # 1
print(counter()) # 2
# 无法从外部修改 count!代码见 ch02_closures.py 中的 make_counter。
问题:如何让函数"记住"自己的状态?闭包是什么?
生活类比:背包旅行
把闭包想象成背包旅行:
┌─────────────────────────────────────────────────────────────┐
│ 闭包 = 背包旅行 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 出发地 = 外层函数 │
│ • 准备行李(定义变量) │
│ • 打包背包(定义内层函数) │
│ • 旅行者出发(返回内层函数) │
│ │
│ 背包 = 闭包 │
│ • 装着出发地的物品(捕获的变量) │
│ • 随旅行者到处移动 │
│ • 外部无法打开(私有变量) │
│ │
│ 旅行者 = 内层函数 │
│ • 背着背包旅行 │
│ • 可以随时使用背包里的物品 │
│ • 记住出发地的状态 │
│ │
│ 示例(ch02_closures.py): │
│ ───────────────────────────────────── │
│ def make_counter(start=0): # 出发地 │
│ count = start # 打包:装进背包 │
│ def increment(): # 旅行者 │
│ nonlocal count │
│ count += 1 # 使用背包里的物品 │
│ return count │
│ return increment # 旅行者出发 │
│ │
│ counter = make_counter(0) # 创建一个旅行者 │
│ counter() # 1,旅行者使用背包 │
│ counter() # 2,旅行者记住上次状态 │
│ │
└─────────────────────────────────────────────────────────────┘一句话:
闭包 = 内层函数 + 被捕获的外层变量(像背包带着物品)
L1 理解层:会用
闭包的三个必要条件
┌─────────────────────────────────────────────────────────────┐
│ 闭包的三个条件 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 嵌套函数 │
│ ───────────────── │
│ def outer(): │
│ def inner(): # 内层函数 │
│ ... │
│ │
│ 2. 内层函数引用外层变量 │
│ ───────────────── │
│ def outer(): │
│ x = 10 │
│ def inner(): │
│ return x # 引用外层变量 │
│ │
│ 3. 外层函数返回内层函数 │
│ ───────────────── │
│ def outer(): │
│ x = 10 │
│ def inner(): │
│ return x │
│ return inner # 返回内层函数 │
│ │
│ 缺一不可: │
│ ───────────────── │
│ ❌ 没有嵌套 → 不是闭包 │
│ ❌ 不引用外层变量 → 没捕获变量 │
│ ❌ 不返回内层函数 → 无法形成闭包 │
│ │
└─────────────────────────────────────────────────────────────┘最简闭包示例
# ch02_closures.py
def make_multiplier(factor: int) -> Callable[[int], int]:
"""创建乘法器 — 最简闭包"""
def multiply(number: int) -> int:
return number * factor # factor 来自闭包
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10(factor=2 被"记住")
print(triple(5)) # 15(factor=3 被"记住")测试验证:test_closure_captures_variable、test_closure_independence
LEGB 变量查找顺序
Python 查找变量时遵循 LEGB 规则,从内到外依次搜索:
┌─────────────────────────────────────────────────────────────┐
│ LEGB 查找顺序 │
├─────────────────────────────────────────────────────────────┤
│ │
│ L — Local(局部) │
│ 当前函数内部的变量 │
│ ↓ 没找到?继续 │
│ E — Enclosing(外层) │
│ 外层函数的变量(闭包捕获的变量) │
│ ↓ 没找到?继续 │
│ G — Global(全局) │
│ 模块级别的变量 │
│ ↓ 没找到?继续 │
│ B — Built-in(内置) │
│ Python 内置函数和异常(len, print, ValueError...) │
│ ↓ 没找到? │
│ NameError! │
│ │
└─────────────────────────────────────────────────────────────┘代码演示
# ch02_closures.py
x: str = "global" # G 层
def outer_enclosed() -> Callable[[], str]:
x = "enclosed" # E 层
def inner() -> str:
return x # 找到 E 层,不再继续查找
return inner
inner = outer_enclosed()
print(inner()) # "enclosed" — 找到 E 层就停止查找过程:
inner内部没有定义x→ L 层没有outer_enclosed定义了x = "enclosed"→ E 层找到,返回- 不再查找 G 层(虽然全局也有
x = "global")
测试验证:test_legb_lookup
nonlocal 关键字
为什么需要 nonlocal
# ❌ 错误:没有 nonlocal,Python 认为你在创建新局部变量
def counter_wrong() -> Callable[[], int]:
count: int = 0
def increment() -> int:
count += 1 # UnboundLocalError!
return count
return increment
# ✅ 正确:使用 nonlocal 声明
def make_counter(start: int = 0) -> Callable[[], int]:
count = start
def increment() -> int:
nonlocal count # 声明使用外层的 count
count += 1
return count
return increment代码见 ch02_closures.py 中的 make_counter。
nonlocal vs global
┌─────────────────────────────────────────────────────────────┐
│ nonlocal vs global │
├─────────────────────────────────────────────────────────────┤
│ │
│ nonlocal │
│ ───────────────── │
│ • 引用外层函数的变量(E层) │
│ • 用于闭包修改捕获的变量 │
│ • 作用范围:当前函数的外层 │
│ │
│ global │
│ ───────────────── │
│ • 用模块级别的变量(G层) │
│ • 用于修改全局变量 │
│ • 作用范围:整个模块 │
│ │
│ 选择: │
│ ───────────────── │
│ • 闭包内修改外层变量 → nonlocal │
│ • 修改全局变量 → global │
│ • 只读取不修改 → 无需声明 │
│ │
└─────────────────────────────────────────────────────────────┘闭包的 closure 属性解剖
闭包捕获的变量可以通过 __closure__ 和 __code__.co_freevars 验证:
# ch02_closures.py
def inspect_closure(func: Callable) -> dict[str, object]:
"""检查闭包的自由变量"""
code = func.__code__
closure = func.__closure__
return {
"freevars": code.co_freevars,
"cell_contents": [cell.cell_contents for cell in closure] if closure else [],
}
# 使用
double = make_multiplier(2)
info = inspect_closure(double)
print(info["freevars"]) # ('factor',)
print(info["cell_contents"]) # [2]| 属性 | 含义 |
|---|---|
func.__code__.co_freevars | 自由变量名元组(被捕获但未在内部定义的变量) |
func.__closure__ | cell 对象元组,每个 cell 存储一个被捕获变量的值 |
cell.cell_contents | 获取 cell 中存储的实际值 |
测试验证:test_inspect_closure
循环变量陷阱
❌ 经典错误
# ch02_closures.py
def create_multipliers_wrong() -> list[Callable[[int], int]]:
"""❌ 循环变量陷阱 — 所有函数共享同一个 i"""
return [lambda x: x * i for i in range(5)]
multipliers = create_multipliers_wrong()
print(multipliers[0](2)) # 8,不是 0!
print(multipliers[1](2)) # 8,不是 2!原因:所有 lambda 都引用同一个 i 变量,循环结束后 i = 4,所以每个函数都用 i = 4 计算。
┌─────────────────────────────────────────────────────────────┐
│ 循环变量陷阱原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [lambda x: x * i for i in range(5)] │
│ │
│ 循环过程: │
│ i=0 → 创建 lambda0,引用变量 i │
│ i=1 → 创建 lambda1,引用变量 i(同一个!) │
│ i=2 → 创建 lambda2,引用变量 i(同一个!) │
│ i=3 → 创建 lambda3,引用变量 i(同一个!) │
│ i=4 → 创建 lambda4,引用变量 i(同一个!) │
│ │
│ 循环结束:i = 4 │
│ │
│ 调用时: │
│ lambda0(2) → 2 * 4 = 8 │
│ lambda1(2) → 2 * 4 = 8 │
│ ... 全都是 8! │
│ │
└─────────────────────────────────────────────────────────────┘✅ 用默认参数修复
# ch02_closures.py
def create_multipliers_correct() -> list[Callable[[int], int]]:
"""✅ 用默认参数捕获当前值"""
return [lambda x, i=i: x * i for i in range(5)]
multipliers = create_multipliers_correct()
print(multipliers[0](2)) # 0 ✓
print(multipliers[1](2)) # 2 ✓
print(multipliers[4](2)) # 8 ✓原理:默认参数在函数定义时求值,每次迭代都会把当前 i 的值绑定到参数 i 上,形成独立的副本。
测试验证:test_closure_loop_trap_wrong、test_closure_loop_trap_correct
贯穿实战:请求计数器
带多种操作的计数器
# ch02_closures.py
def make_counter_with_ops(start: int = 0) -> dict[str, Callable]:
"""带多种操作的计数器 — 返回函数字典"""
count = start
def increment() -> int:
nonlocal count
count += 1
return count
def decrement() -> int:
nonlocal count
count -= 1
return count
def get_value() -> int:
return count
def reset() -> int:
nonlocal count
count = 0
return count
return {
"increment": increment,
"decrement": decrement,
"get": get_value,
"reset": reset,
}
# 使用
ops = make_counter_with_ops(10)
print(ops["increment"]()) # 11
print(ops["decrement"]()) # 10
print(ops["get"]()) # 10
print(ops["reset"]()) # 0关键代码说明:
| 代码 | 含义 | 为什么这样写 |
|---|---|---|
nonlocal count | 声明使用外层变量 | Python 默认创建新局部变量,需要声明 |
count = start | 外层函数定义变量 | 被闭包捕获的私有变量 |
return {...} | 返回多个操作函数 | 提供多种操作方式 |
make_counter() | 创建闭包实例 | 每次调用创建独立状态 |
测试验证:test_counter_with_ops、test_two_counters_independent
L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| 用闭包替代全局变量 | 避免状态污染,外部无法修改 | make_counter() |
| 用 nonlocal 修改外层变量 | 必需语法,否则 UnboundLocalError | nonlocal count |
| 返回多个操作函数 | 灵活操作,类似 OOP 的方法集合 | {"get": get, "reset": reset} |
| 验证闭包变量 | 确保捕获正确 | func.__code__.co_freevars |
| 循环中创建闭包用默认参数 | 避免共享变量陷阱 | lambda x, i=i: ... |
| 每个 make_xxx() 调用创建独立状态 | 不同实例互不影响 | c1 = make_counter(0); c2 = make_counter(100) |
反模式:不要这样做
# ❌ 错误:用 global 替代 nonlocal(污染全局作用域)
count = 0
def increment():
global count
count += 1
return count
# ✅ 正确:用闭包封装私有状态
def make_counter(start=0):
count = start
def increment():
nonlocal count
count += 1
return count
return increment
# ❌ 错误:在闭包内修改外层变量不用 nonlocal
def counter_broken():
count = 0
def increment():
count += 1 # UnboundLocalError
return count
return increment
# ❌ 错误:循环中创建 lambda 忘记默认参数绑定
handlers = [lambda: i for i in range(5)] # 全部返回 4
# ✅ 正确:
handlers = [lambda i=i: i for i in range(5)]
# ❌ 错误:闭包内修改可变对象但不用 nonlocal(修改容器元素不需要 nonlocal,但赋值需要)
def make_list_builder():
items = []
def add(x):
items.append(x) # ✅ 不需要 nonlocal(修改对象内容)
return items
def replace(new_items):
items = new_items # ❌ 赋值操作创建了新的局部变量!
return items
return add, replace
# ✅ 正确:赋值时使用 nonlocal
def make_list_builder_fixed():
items = []
def add(x):
items.append(x)
return items
def replace(new_items):
nonlocal items
items = new_items
return items
return add, replace适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 计数器/累加器 | ✅ 推荐 | 闭包非常适合需要维护私有状态的场景 |
| 配置封装 | ✅ 推荐 | 将配置参数封装到闭包中,避免全局变量 |
| 回调函数注册 | ✅ 推荐 | 每个闭包可以携带自己的上下文 |
| 简单的类替代 | ✅ 推荐 | 只有一个方法的状态管理可用闭包替代类 |
| 复杂状态管理 | ❌ 不推荐 | 多状态、多方法的场景更适合用类 |
| 需要序列化 | ❌ 不推荐 | 闭包无法被 pickle 序列化 |
| 需要继承/多态 | ❌ 不推荐 | 闭包不支持继承 |
L3 专家层:深入
Python 如何实现:Cell 对象与闭包变量绑定
在 CPython 中,闭包引用外层变量时,使用的不是直接引用,而是通过 cell 对象间接引用:
┌─────────────────────────────────────────────────────────────┐
│ Cell 对象 — 闭包变量存储机制 │
│ │
│ def outer(): │
│ x = 10 │
│ y = 20 │
│ def inner(): │
│ return x + y # x 和 y 都是自由变量 │
│ return inner │
│ │
│ f = outer() │
│ │
│ f 的内部结构: │
│ ┌──────────────────────────────────────────┐ │
│ │ f (PyFunctionObject) │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ __code__ │ │ │
│ │ │ .co_freevars │ → ('x', 'y') │ │
│ │ │ .co_cellvars │ → () │ │
│ │ └─────────────────────┘ │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ __closure__ (cell 对象元组) │ │ │
│ │ │ ┌───────┐ ┌───────┐ │ │ │
│ │ │ │ cell0 │ │ cell1 │ │ │ │
│ │ │ │ .cell_│ │ .cell_│ │ │ │
│ │ │ │contents│ │contents│ │ │ │
│ │ │ │ = 10 │ │ = 20 │ │ │ │
│ │ │ └───────┘ └───────┘ │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 关键: │
│ • cell 对象 = 一层"包装",用于间接引用变量 │
│ • 多个内层函数可以共享同一个 cell 对象 │
│ • 修改 cell.cell_contents 会影响到所有共享该 cell 的函数 │
│ • 这是 Python 在解释器层面解决 outer 函数作用域已退出 │
│ 后变量仍然存活的问题 │
│ │
└─────────────────────────────────────────────────────────────┘验证:cell 对象共享
def outer():
x = 10
def getter():
return x
def setter(v):
nonlocal x
x = v
return getter, setter
getter, setter = outer()
# getter 和 setter 共享同一个 cell 对象
print(getter.__closure__[0] is setter.__closure__[0]) # True!
# 通过 setter 修改后,getter 也能读到新值
setter(42)
print(getter()) # 42(通过共享的 cell 同步)
# cell 对象内部
cell = getter.__closure__[0]
print(cell.cell_contents) # 42字节码层面:LOAD_DEREF / STORE_DEREF
import dis
def outer():
x = 10
def inner():
nonlocal x
x += 1 # 读取 + 赋值
return x
return inner
f = outer()
# 查看 inner 的字节码
dis.dis(f)
# 输出:
# 6 0 RESUME 0
# 7 2 LOAD_DEREF 0 (x) ← 通过 cell 读取
# 4 LOAD_CONST 1 (1)
# 6 BINARY_OP 13 (+=)
# 10 STORE_DEREF 0 (x) ← 通过 cell 写入
# 12 LOAD_DEREF 0 (x) ← 再次读取返回
# 14 RETURN_VALUE| 字节码指令 | 含义 | 使用场景 |
|---|---|---|
LOAD_FAST | 从局部变量读取 | 函数内部定义的变量 |
STORE_FAST | 写入局部变量 | x = 1 |
LOAD_DEREF | 从 cell 对象读取 | 闭包引用外层变量 |
STORE_DEREF | 写入 cell 对象 | nonlocal x; x = 1 |
LOAD_GLOBAL | 从全局变量读取 | 模块级别变量 |
循环变量陷阱的 cell 视角:在 for i in range(5) 中创建多个闭包时,所有闭包的 __closure__[0] 都指向同一个 cell 对象。循环每次迭代更新 cell.cell_contents,最后一个值覆盖所有。
性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
func() 调用闭包 | O(1) | 与普通函数调用几乎无差,通过 cell 访问变量 |
LOAD_DEREF | O(1) | cell 对象是单槽容器,读取即解引用 |
nonlocal count += 1 | O(1) | LOAD_DEREF + 计算 + STORE_DEREF |
| 创建闭包实例 | O(1) | 创建 function 对象 + cell 元组引用 |
inspect_closure() 遍历 | O(n) | n = 自由变量个数 |
| 场景 | 闭包 | 类实例 | 说明 |
|---|---|---|---|
| 内存占用(1个计数变量) | ~72 bytes | ~56 bytes | 单变量闭包通常更轻量 |
| 内存占用(10个变量) | ~200 bytes | ~120 bytes | 多变量时类更紧凑 |
| 属性访问速度 | ~35ns (LOAD_DEREF) | ~50ns (LOAD_ATTR) | 闭包的 cell 访问比属性访问快 |
| 创建速度 | ~150ns | ~300ns | 闭包创建更快(无需 init) |
知识关联
┌─────────────────────────────────────────────────────────────┐
│ 知识关联图:闭包 → 装饰器 → 高阶函数 │
│ │
│ 闭包的核心能力 │
│ ┌──────────────────────────────────────┐ │
│ │ • 记住外层变量的值(状态保持) │ │
│ │ • 通过 cell 对象共享变量 │ │
│ │ • 访问控制(私有变量) │ │
│ └──────────┬───────────────────────────┘ │
│ │ │
│ ┌────────┼──────────────────┐ │
│ ↓ ↓ ↓ │
│ 装饰器 装饰器工厂 偏函数 │
│ 第3章 第4章 第5章 │
│ (闭包 (三层嵌套 (functools.partial) │
│ 捕获func) 捕获参数+func) │
│ │
│ Python 标准库中的闭包应用: │
│ ───────────────────────────────── │
│ • functools.partial → 偏函数内部使用闭包固定参数 │
│ • operator.itemgetter(n) → 返回捕获 n 的闭包 │
│ • contextlib.contextmanager → 装饰器内部使用闭包 │
│ • unittest.mock.patch → 闭包捕获 mock 对象 │
│ │
│ nonlocal 的替代表达: │
│ ───────────────────── │
│ • 用可变容器(如 list[0])绕过 nonlocal: │
│ count = [0] │
│ def inc(): count[0] += 1; return count[0] │
│ # 不需要 nonlocal,因为修改的是列表内容 │
│ • 不推荐:是历史遗留方案,nonlocal 更清晰 │
│ │
└─────────────────────────────────────────────────────────────┘自检清单
回答以下问题,检查你是否掌握了核心概念:
- LEGB 查找顺序是什么?闭包在哪个层找到变量?
- 闭包的三个必要条件是什么?
nonlocal和global有什么区别?- 如何验证闭包捕获了哪些变量?
- 循环变量陷阱的根因是什么?如何修复?
答案:
- Local → Enclosing → Global → Built-in;闭包在 E 层(Enclosing)找到变量
- 嵌套函数、内层引用外层变量、外层返回内层函数
- nonlocal 引用外层函数变量(E层),global 引用模块变量(G层)
- 检查
func.__code__.co_freevars和func.__closure__ - 所有 lambda 共享同一个循环变量;用默认参数
lambda x, i=i:在定义时捕获当前值
本章能力清单
学完本章,你能够:
- [x] 理解 LEGB 变量查找规则
- [x] 理解闭包的三个必要条件
- [x] 用闭包创建私有状态(计数器、乘法器)
- [x] 使用
nonlocal修改外层变量 - [x] 区分
nonlocal和global - [x] 验证闭包是否捕获了变量(
__closure__) - [x] 识别并修复循环变量陷阱