Skip to content

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 系统,需要统计每个路由的请求次数:

python
# ❌ 方案一:全局变量(容易被篡改)
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  # 返回内层函数                           │
│                                                             │
│  缺一不可:                                                  │
│  ─────────────────                                          │
│  ❌ 没有嵌套 → 不是闭包                                      │
│  ❌ 不引用外层变量 → 没捕获变量                              │
│  ❌ 不返回内层函数 → 无法形成闭包                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

最简闭包示例

python
# 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_variabletest_closure_independence

LEGB 变量查找顺序

Python 查找变量时遵循 LEGB 规则,从内到外依次搜索:

┌─────────────────────────────────────────────────────────────┐
│  LEGB 查找顺序                                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  L — Local(局部)                                           │
│      当前函数内部的变量                                       │
│      ↓ 没找到?继续                                          │
│  E — Enclosing(外层)                                       │
│      外层函数的变量(闭包捕获的变量)                         │
│      ↓ 没找到?继续                                          │
│  G — Global(全局)                                          │
│      模块级别的变量                                          │
│      ↓ 没找到?继续                                          │
│  B — Built-in(内置)                                        │
│      Python 内置函数和异常(len, print, ValueError...)       │
│      ↓ 没找到?                                              │
│  NameError!                                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

代码演示

python
# 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 层就停止

查找过程:

  1. inner 内部没有定义 x → L 层没有
  2. outer_enclosed 定义了 x = "enclosed"E 层找到,返回
  3. 不再查找 G 层(虽然全局也有 x = "global"

测试验证test_legb_lookup

nonlocal 关键字

为什么需要 nonlocal

python
# ❌ 错误:没有 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 验证:

python
# 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

循环变量陷阱

❌ 经典错误

python
# 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!                                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

✅ 用默认参数修复

python
# 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_wrongtest_closure_loop_trap_correct

贯穿实战:请求计数器

带多种操作的计数器

python
# 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_opstest_two_counters_independent


L2 实践层:用好

推荐做法

做法原因示例
用闭包替代全局变量避免状态污染,外部无法修改make_counter()
用 nonlocal 修改外层变量必需语法,否则 UnboundLocalErrornonlocal count
返回多个操作函数灵活操作,类似 OOP 的方法集合{"get": get, "reset": reset}
验证闭包变量确保捕获正确func.__code__.co_freevars
循环中创建闭包用默认参数避免共享变量陷阱lambda x, i=i: ...
每个 make_xxx() 调用创建独立状态不同实例互不影响c1 = make_counter(0); c2 = make_counter(100)

反模式:不要这样做

python
# ❌ 错误:用 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 对象共享

python
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

python
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_DEREFO(1)cell 对象是单槽容器,读取即解引用
nonlocal count += 1O(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 更清晰                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

自检清单

回答以下问题,检查你是否掌握了核心概念:

  1. LEGB 查找顺序是什么?闭包在哪个层找到变量?
  2. 闭包的三个必要条件是什么?
  3. nonlocalglobal 有什么区别?
  4. 如何验证闭包捕获了哪些变量?
  5. 循环变量陷阱的根因是什么?如何修复?

答案

  1. Local → Enclosing → Global → Built-in;闭包在 E 层(Enclosing)找到变量
  2. 嵌套函数、内层引用外层变量、外层返回内层函数
  3. nonlocal 引用外层函数变量(E层),global 引用模块变量(G层)
  4. 检查 func.__code__.co_freevarsfunc.__closure__
  5. 所有 lambda 共享同一个循环变量;用默认参数 lambda x, i=i: 在定义时捕获当前值

本章能力清单

学完本章,你能够:

  • [x] 理解 LEGB 变量查找规则
  • [x] 理解闭包的三个必要条件
  • [x] 用闭包创建私有状态(计数器、乘法器)
  • [x] 使用 nonlocal 修改外层变量
  • [x] 区分 nonlocalglobal
  • [x] 验证闭包是否捕获了变量(__closure__
  • [x] 识别并修复循环变量陷阱