Python陷阱大全:闭包延迟绑定与循环变量捕获
请深入分析Python闭包陷阱的多种表现形式:for循环中的lambda、列表推导式中的lambda、threading回调中的延迟绑定。给出每种场景的解决方案并解释其字节码层面的原因。
回答
屠龙少年
经典场景
def create_functions():
funcs = []
for i in range(4):
funcs.append(lambda: i)
return funcs
for f in create_functions():
print(f(), end=' ') # 3 3 3 3
字节码层面: lambda内部有LOAD_DEREF指令加载自由变量i。i存储在单元对象(cell object)中,闭包捕获的是cell的引用,而不是值的拷贝。循环结束后cell中的值为3。
列表推导式中的lambda
funcs = [lambda: i * i for i in range(4)]
for f in funcs:
print(f(), end=' ') # 9 9 9 9
Python 3.x中列表推导式有自己的作用域,但变量i仍然是在推导式的局部作用域中,所有lambda共享同一个i。
解决方案总结
方案1:默认参数绑定(最常用)
for i in range(4):
funcs.append(lambda i=i: i) # 默认参数在定义时求值
方案2:functools.partial
from functools import partial
for i in range(4):
funcs.append(partial(lambda x: x, i))
方案3:闭包工厂
def make_func(x):
return lambda: x
funcs = [make_func(i) for i in range(4)]
# 输出:0 1 2 3
每个make_func调用创建新的作用域,x在每个作用域中独立。
方案4:生成器替代
def generate():
for i in range(4):
yield lambda: i
for f in generate():
print(f(), end=' ') # 0 1 2 3
生成器使用yield而不是return,保留了每次迭代时的状态。
核心原则: 不要让闭包共享可变循环变量,使用默认参数或工厂函数创建独立作用域。