字典推导与常规字典创建语义差异:<dictcomp>.<lambda>行为异常解析
哈哈,这个问题其实是Python里闭包常见的“坑”,和字典推导本身关系不大,核心是闭包变量的延迟绑定特性在搞鬼!
你看,你在字典推导里定义的lambda: print(name),这里的name并不是在每次迭代时就被“固定”下来的。Lambda函数在定义的时候,并不会直接把当前name的值存进去,而是记住了要去访问这个变量的引用。只有当你真正调用这个lambda的时候,它才会去查找name当前的取值——而字典推导的循环结束后,name变量最后指向的就是列表里的最后一个元素"bob",所以不管是d["a"]()还是d["b"](),调用时都会取到这个最终值。
至于你看到两个lambda的id不同,这很正常:它们确实是两个独立的函数对象,但它们捕获的是同一个外部作用域里的name变量引用。就像两个不同的人都盯着同一个杯子,杯子里最后装的是bob,那他们看到的自然都是bob。
要解决这个问题,你需要在定义lambda的时候,就把当前迭代的name值“固定”下来。最常用的方法是把name作为lambda的默认参数,因为默认参数的值是在函数定义时就确定的,而不是调用时:
d = { **{ keyword: lambda n=name: print(n) for (keyword, name) in [ ("a", "alice"), ("b", "bob"), ] }, "c": lambda: print("carol") }
这样修改后,d["a"]()就会输出"alice",d["b"]()输出"bob"了——因为每个lambda的默认参数n在定义时就绑定了当前迭代的name值,和外部的name变量不再有关系。
再说说你提到的生成器的点:字典推导确实是基于生成器实现的,但生成器的迭代过程中,循环变量name是在同一个作用域里的,并不会每次迭代都创建一个新的name变量。这和普通for循环里定义lambda的问题完全一样:
funcs = [] for keyword, name in [("a", "alice"), ("b", "bob")]: funcs.append(lambda: print(name)) funcs[0]() # 输出bob funcs[1]() # 输出bob
字典推导只是把这个循环写成了紧凑的形式,作用域逻辑和普通for循环没区别。
总之,记住这个关键点:闭包(包括lambda)里的变量是延迟绑定的,调用时才会取当前值;要绑定定义时的值,就用默认参数或者其他方式把值“封装”到函数内部。
内容的提问来源于stack exchange,提问作者Atonal

