Python 闭包是什么?为什么循环里的闭包会出现延迟绑定问题?
简化版
闭包是内部函数引用了外部函数的变量,并且在外部函数执行结束后仍然能访问这些变量。Python 闭包捕获的是变量名绑定关系,不是当时的值,所以循环里创建函数时容易出现延迟绑定问题。
详细版
闭包示例:
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
double = make_multiplier(2)
print(double(10)) # 20
multiply 引用了外层变量 n,即使 make_multiplier 已经执行结束,n 仍然被保留。
高频坑:
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2]
原因是 lambda 里引用的是变量 i,而不是每次循环时的值。循环结束后 i 的值是 2,所以所有函数调用时都读到 2。
常见修复方式:
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
print([f() for f in funcs]) # [0, 1, 2]
也可以用工厂函数:
def make_func(i):
def f():
return i
return f
完整版教学
一、闭包解决什么问题
闭包可以让函数携带一部分上下文,形成一个“带状态的函数”。
例如生成不同倍数的乘法器:
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
调用:
triple = make_multiplier(3)
print(triple(10)) # 30
triple 本质上是 multiply 函数,但它记住了外层的 n=3。
闭包常用于:
- 装饰器;
- 回调函数;
- 函数工厂;
- 延迟执行;
- 保存轻量状态。
二、闭包成立的三个条件
一个典型闭包通常满足三点:
- 存在嵌套函数。
- 内部函数引用外部函数的变量。
- 外部函数返回内部函数,或内部函数在外部作用域之外继续被使用。
示例:
def outer():
msg = "hello"
def inner():
return msg
return inner
inner 引用了 msg,并被返回到外部,所以 msg 不会随着 outer 结束立刻消失。
三、闭包变量存在哪里
可以通过 __closure__ 观察闭包保存的变量:
def outer():
x = 10
def inner():
return x
return inner
f = outer()
print(f.__closure__[0].cell_contents) # 10
这说明闭包变量被放在 cell 对象中,内部函数通过这个 cell 找到外层变量。
这也是为什么闭包不是简单复制值。它保留的是对变量所在 cell 的引用。
四、延迟绑定为什么出现
看这个例子:
funcs = []
for i in range(3):
funcs.append(lambda: i)
循环创建了三个函数,但这些函数体没有立刻执行。等你之后调用:
[f() for f in funcs]
函数才去查找 i。此时循环已经结束,i 最终绑定的是 2,所以三个函数都返回 2。
这叫延迟绑定:函数使用变量时才解析变量值,而不是定义函数那一刻就把值固定下来。
五、如何修复延迟绑定
方法一:使用默认参数固定当前值。
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
默认参数在函数定义时求值,所以每个 lambda 都有自己的默认值。
方法二:使用工厂函数。
def make_func(i):
def f():
return i
return f
funcs = [make_func(i) for i in range(3)]
每次调用 make_func(i) 都会创建新的局部作用域,每个 f 绑定到不同作用域里的 i。
方法三:用 functools.partial 固定参数。
from functools import partial
def identity(i):
return i
funcs = [partial(identity, i) for i in range(3)]
六、闭包和对象的选择
闭包适合保存少量状态:
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
如果状态复杂、方法很多、需要继承或清晰的数据结构,就更适合用类:
class Counter:
def __init__(self):
self.count = 0
def inc(self):
self.count += 1
return self.count
闭包不是类的替代品,它更像轻量函数状态封装。
七、常见误区与追问
记忆钩子:闭包不是“把值拍照存档”,而是“保留能找到变量的环境”。循环延迟绑定的坑,就来自多个函数最后看向同一个变量 cell。
- 误区:闭包一定会复制外层变量的值。 Python 闭包保存的是对外层变量 cell 的引用,调用函数时再通过 cell 取值;这就是循环里多个 lambda 最后都读到
2的根本原因。 - 追问:为什么默认参数能修复循环闭包? 默认参数在函数定义时求值,
lambda i=i: i会把当次循环的i值放进该函数自己的默认参数元组里,不再依赖循环变量后续变化。 - 误区:只有 lambda 才有延迟绑定问题。 用
def在循环里定义内部函数同样会遇到这个问题;问题来自闭包变量解析时机,不来自lambda语法。 - 追问:闭包变量能不能修改? 能,但要在内部函数里用
nonlocal声明,否则count += 1会被解释成局部赋值并触发UnboundLocalError。 - 误区:闭包比类更高级,所以应该优先闭包。 闭包适合少量状态和单一行为;当状态多、行为多、需要继承或调试可见性时,类通常更清晰。
- 追问:怎么观察一个函数是不是闭包? 可以看
func.__closure__是否为None,以及其中 cell 的cell_contents;但工程代码不应依赖这个内部细节做业务逻辑。
八、加强记忆
闭包记住的是外层变量所在的环境,不是简单把值复制一份。循环闭包的坑来自延迟绑定:函数调用时才读取变量,所以循环结束后大家读到同一个最终值。要固定当次循环值,就用默认参数、工厂函数或 partial。