复现循环创建闭包的经典陷阱,用__closure__查看底层的cell对象,验证用默认参数在定义时绑定值能修复这个问题。
闭包是一个函数,加上它记住的外部变量。
代码def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment c1 = make_counter() c2 = make_counter()
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跑出来的原始结果。
__closure__ / cell_contents:查看闭包底层的cell对象