>>>PyPathPython 学习站
首页›进阶篇›kp-023
进阶篇 · Advanced lv.3 进阶 kp-023

闭包与作用域

前置知识:kp-007

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. 常见误区

  1. 以为闭包复制了值 —— 捕获变量本身;外层后续修改对内层可见。
  2. 循环变量 late binding(上文)—— 记住"lambda 捕获的是名字不是值",修复用默认参数或 functools.partial。
  3. 在内层只读不写时也写 nonlocal —— 只读不需要声明;nonlocal 只为赋值服务。
  4. 把闭包状态当持久化 —— 进程结束即消失;需要持久化用文件/DB(kp-016)。

10. 自测题

  1. funcs = [lambda: i for i in range(3)] 为什么三次调用都返回 2?
  2. __closure__ 是什么?什么时候为 None?
  3. 闭包与"用类实现计数器"的取舍?
参考答案
  1. lambda 捕获的是循环变量 i 的绑定;推导式结束后 i 停留在 2,所有 lambda 共享。
  2. 被捕获变量的 cell 元组;内层函数没有引用任何外层变量时为 None。
  3. 状态少、行为单一用闭包更轻;状态多、需要多方法/继承/可测试注入时用类。

11. 与其他知识点的关系

  • kp-024 装饰器:闭包最著名的应用。
  • kp-033 内存:闭包延长对象生命周期,可能造成"内存滞留"。
  • kp-013 生成器:另一种"函数携带状态"的方案,可对照理解。

12. 延伸阅读

  • Fluent Python 第 7 章(函数作为一等对象)
  • PEP 227(Statically Nested Scopes)