DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Python 循环创建的 lambda 回调全部读到最后一个值

在函数创建时绑定循环值,保留回调实参约定,并区分对象引用与内容快照,避免延迟调用时串用标识。

在循环里创建多个回调,循环结束后逐个调用,却都得到最后一次循环的值:

def make_callbacks():
    callbacks = []
    for value in range(3):
        callbacks.append(lambda: value)
    return callbacks

callbacks = make_callbacks()
print([callback() for callback in callbacks])
# 预期为 [2, 2, 2],不是 [0, 1, 2]

Python 3 中,函数体里的外层变量在调用时才被读取。这里三个函数都访问同一次 make_callbacks() 调用中的 value;它们没有各自保存创建那一刻的数字。循环结束时 value 已经是 2,所以结果相同。

在创建时绑定当前值

对不接收参数的简单回调,可以利用默认参数在函数定义时求值的规则:

def make_callbacks():
    callbacks = []
    for value in range(3):
        callbacks.append(lambda saved=value: saved)
    return callbacks

callbacks = make_callbacks()
print([callback() for callback in callbacks])
# 预期为 [0, 1, 2]

右侧的 value 在本轮创建函数时读取,结果成为该函数参数 saved 的默认值。函数体返回的是自己的参数,不再读取不断变化的循环变量。左、右两边使用不同名称,有助于区分“默认值表达式”和“调用时的局部参数”。

把循环里的 lambda 改写成普通 def,但函数体仍直接读取外层 value,不会自动解决问题。同样,把回调从列表换成字典保存,也不会改变名字解析的时机。

回调框架会传参数时,保留它的调用约定

默认参数只在调用方没有传入相应实参时生效。若框架会调用 callback(event),直接写 lambda saved=value: saved 会让事件对象覆盖 saved。

下面明确把事件与预先绑定的标识分开:

def make_callbacks(handle):
    callbacks = []
    for item_id in range(3):
        callbacks.append(
            lambda event, saved_id=item_id: handle(event, saved_id)
        )
    return callbacks

此版本约定框架传一个事件参数,handle 接收事件和标识。实际框架若传更多参数,应按它的真实签名调整;不要为消除异常随意吞掉参数,让业务处理得到错误对象。

需要保持回调严格无参数时,可以用工厂函数建立独立绑定:

def make_callback(value):
    def callback():
        return value
    return callback

callbacks = [make_callback(value) for value in range(3)]
print([callback() for callback in callbacks])
# 预期为 [0, 1, 2]

每次 make_callback(value) 都产生一次独立函数调用,其局部 value 由对应的回调保留。不要只在同一个循环里给外层变量换个名字;仍然共用一个不断赋值的名字时,问题会留下来。

绑定对象不等于复制对象

默认参数和工厂函数保存的都可能是同一个可变对象的引用。若绑定的是列表、字典或业务对象,随后原地修改它,回调仍能看到该对象的新内容。

因此要区分两个要求:保存“当时选中了哪个对象”,与保存“当时对象全部内容的快照”。前者通常只需上述绑定;后者需要根据对象结构和业务语义决定是否复制,浅拷贝也不能隔离所有嵌套成员。不要为修复整数示例,把复杂业务对象无条件深拷贝。

验证延迟调用,而不只验证循环内部

测试时先完成回调创建,再逐个调用,确认各回调返回对应的标识。若总在循环的当前迭代内立刻调用,外层变量尚未变化,可能暂时看不出问题。事件回调还需验证实际框架传入参数时,预先绑定的标识不会被覆盖。

修改源代码后,已经注册的旧回调对象不会自动变成新函数。按应用正常流程重新构建或注销后注册回调,避免新旧处理器同时触发;不要直接重复注册造成一次事件执行两遍。保留原注册逻辑与签名可回退,涉及写入或通知的回调应使用无副作用替身核对参数,不靠反复触发真实业务来验证。