bigywater.tech
Python · 15

闭包与自由变量:函数为什么能记住外部变量

复现循环创建闭包的经典陷阱,用__closure__查看底层的cell对象,验证用默认参数在定义时绑定值能修复这个问题。

独立cell
每次调用外层函数创建独立的闭包状态
[2,2,2]陷阱
循环闭包捕获的是同一个变量
i=i修复
默认参数在定义时立刻绑定值

做的事:两个独立的计数器闭包,各自记住自己的状态

闭包是一个函数,加上它记住的外部变量。

代码def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment c1 = make_counter() c2 = make_counter()

验证:每次调用make_counter,创建一份独立的count

c1和c2互不干扰。

结果c1独立调用3次: 1 2 3 c2是另一个独立的闭包,从0开始: 1 2 c1.__closure__: (<cell at 0x...: int object at 0x...>,) c1.__closure__[0].cell_contents 是: 3

验证了闭包底层是怎么"记住"外部变量的:increment 这个函数对象带着一个 __closure__ 元组,里面是一个个 cell 对象——每个cell是一个小盒子,装着被闭包捕获的那个自由变量当前的值。c1 和 c2 是两次不同的 make_counter() 调用,各自创建了独立的 count 变量,也就各自拥有独立的cell,互不干扰。

复现经典陷阱:循环里创建闭包,全部捕获的是同一个变量

这是一个极容易踩的坑。

代码与结果funcs = [] for i in range(3): funcs.append(lambda: i) 结果(期望0,1,2,但实际是): [2, 2, 2]

原因是:闭包捕获的是变量 i 本身(是同一个cell),不是循环当时那一刻 i 的值。循环结束时 i 最终停在2,之后不管调用哪个lambda,它们读的都是同一个cell里当前的值——也就是循环结束后遗留的2。

修复:用默认参数在函数定义那一刻求值funcs2 = [] for i in range(3): funcs2.append(lambda i=i: i) 修复后的结果: [0, 1, 2]
默认参数 i=i 的关键在于:函数定义的那一刻(每次循环都会重新定义一次lambda),右边的 i 会被求值并存进这个新函数自己的默认参数里,不再依赖外层循环变量后续会不会变——默认参数值在定义时就被冻结了,闭包对自由变量的捕获则是"实时读取",这是两种完全不同的绑定时机。

分步演示:闭包是怎么记住(以及误记)外部变量的

每一步都是本文上面用Python 3.14跑出来的原始结果。

闭包捕获变量本身,不是当时的值
这一步新验证的结果

    本文用到的工具

    • 本机 Python 3.14.7
    • __closure__ / cell_contents:查看闭包底层的cell对象
    京ICP备2026067502号-1京公网安备11010502063487号