首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >lambda表达式在循环里偷了我的数据,这个坑太隐蔽了

lambda表达式在循环里偷了我的数据,这个坑太隐蔽了

原创
作者头像
风一样的男子
发布2026-07-15 15:42:32
发布2026-07-15 15:42:32
980
举报
文章被收录于专栏:编程教程编程教程

一个让我在GUI程序上崩溃的Bug

去年我在写一个简单的计算器GUI,用Tkinter做界面。界面上有一排数字按钮(0-9),每个按钮点击后把对应的数字输入到文本框里。代码是这样的:

代码语言:javascript
复制
from tkinter import Button, Tk

root = Tk()
for i in range(10):
    btn = Button(root, text=str(i), command=lambda: print(i))
    btn.pack()
root.mainloop()

看起来没问题吧?每个按钮都有自己对应的数字。我信心满满地运行,然后挨个点了0到9的按钮——所有按钮打印出来的都是9

我当时脑子里只有一句话:“我明明给每个lambda传了不同的i,为什么全是9?”

更让我崩溃的是,这个问题不是偶然出现的,而是Python闭包的一个经典特性。我那一个下午都在跟这个“偷数据”的lambda作斗争。


先复现这个“灵异”现象

不用GUI,直接用最简单的代码就能复现:

代码语言:javascript
复制
funcs = []
for i in range(5):
    funcs.append(lambda: i)

for f in funcs:
    print(f(), end=" ")  # 输出: 4 4 4 4 4

你期望的可能是0 1 2 3 4,但实际输出全是4

再换一种写法:

代码语言:javascript
复制
funcs = [lambda: i for i in range(5)]
for f in funcs:
    print(f(), end=" ")  # 同样是 4 4 4 4 4

结果一样。所有lambda都“偷”了同一个数据——循环结束后的最终值i=4

这个现象叫作延迟绑定(Late Binding)。lambda表达式捕获的不是变量的,而是变量的引用。当lambda最终被调用时,循环早已结束,变量i的值已经变成了最后一个循环值。


为什么lambda会“偷”数据?

要理解这个问题,先搞清楚两个概念:

1. 闭包(Closure)

当一个函数(这里是lambda)引用了它所在作用域之外的变量时,就形成了一个闭包。lambda: i里的i,就是来自外层(循环所在的作用域)的变量。

2. 延迟绑定

Python的闭包捕获的是变量本身(引用),而不是变量在创建时刻的。也就是说,lambda记住的是“我要去外层作用域找i这个变量”,而不是“我创建时i的值是几”。

所以当你调用lambda时,Python会去外层作用域查找i当前值。而循环结束后,i的当前值是4(因为range(5)的最后一个值是4)。所有lambda都指向同一个i,于是全都打印4。


这和循环的变量作用域也有关系

Python没有块级作用域。循环变量i在循环结束后依然存在,它属于循环所在的整个函数或模块。

代码语言:javascript
复制
for i in range(5):
    pass
print(i)  # 4 —— i依然活着

所有lambda共享同一个i。如果循环结束后你修改了i,lambda的值也会跟着变:

代码语言:javascript
复制
funcs = [lambda: i for i in range(5)]
i = 100
for f in funcs:
    print(f(), end=" ")  # 100 100 100 100 100

看到了吗?lambda不是“记住”了0-4,而是每次都在问“i现在是什么”。


不只是lambda,普通函数也一样

这个问题不限于lambda,普通函数同样会中招:

代码语言:javascript
复制
funcs = []
for i in range(5):
    def f():
        return i
    funcs.append(f)

for f in funcs:
    print(f(), end=" ")  # 同样全是4

所以问题的本质是Python闭包的延迟绑定,跟是不是lambda没关系,lambda只是让代码更短,更容易写出这个问题。


怎么解决?四种方法,总有一款适合你

方法1:默认参数快照(最常用)

把当前i的值作为默认参数传给lambda,默认参数在函数定义时求值,而不是调用时。

代码语言:javascript
复制
funcs = []
for i in range(5):
    funcs.append(lambda i=i: i)  # i作为默认参数

for f in funcs:
    print(f(), end=" ")  # 0 1 2 3 4

原理lambda i=i: i中,i=i表示把当前循环的i值作为默认参数保存下来。默认参数在定义时就被计算并固定了。每个lambda都有自己独立的默认参数,互不影响。

方法2:用functools.partial固定参数

partial可以把函数的某些参数提前固定下来,返回一个新的可调用对象。

代码语言:javascript
复制
from functools import partial

def func(i):
    return i

funcs = []
for i in range(5):
    funcs.append(partial(func, i))

for f in funcs:
    print(f(), end=" ")  # 0 1 2 3 4

partial(func, i)在循环执行时就计算出i的值,然后把它固定下来。

方法3:工厂函数(闭包套闭包)

用一个外层函数把i“捕获”住,变成一个局部的、独立的值。

代码语言:javascript
复制
def create_func(i):
    return lambda: i

funcs = []
for i in range(5):
    funcs.append(create_func(i))

for f in funcs:
    print(f(), end=" ")  # 0 1 2 3 4

create_func(i)执行时,参数i是传进去的,这个值被保存在create_func的作用域里。每个lambda捕获的是不同create_func调用中的i,因此各自独立。

方法4:用生成器或列表推导式的“内部作用域”

在Python 3中,列表推导式有自己的作用域,所以下面这种写法其实不会出问题:

代码语言:javascript
复制
funcs = [lambda: i for i in range(5)]  # 依然有延迟绑定,会出问题

等等,上面这个写法我们刚才验证过,会出问题。那正确的写法是什么?

实际上,列表推导式里的i作用域只在推导式内部,但lambda捕获的仍然是这个i——延迟绑定依然存在。所以方法4不是一个有效解法。

真正的第四种方法是map或显式循环代替lambda列表,但实用性不高。建议优先用前三种。


真实场景:GUI按钮的惨案

回到开头的Tkinter例子。正确的写法应该是:

代码语言:javascript
复制
from tkinter import Button, Tk

root = Tk()
for i in range(10):
    # 方法1:默认参数快照
    btn = Button(root, text=str(i), command=lambda i=i: print(i))
    btn.pack()
root.mainloop()

这样每个按钮点击后打印自己的数字,再也不乱了。


一张表总结

场景

错误写法

正确写法

循环内创建lambda

lambda: i

lambda i=i: i

循环内创建普通函数

def f(): return i

def f(i=i): return i

用partial固定参数

lambda: func(i)

partial(func, i)

工厂函数

直接循环

def make(i): return lambda: i


回到开头的Bug

那个计算器程序,我最后用了默认参数快照:command=lambda i=i: print(i)。所有按钮都能正确打印自己的数字了。

记住:在Python里,循环中的lambda或嵌套函数不会“记住”当前循环变量的值,它们只会记住变量的引用。当函数被调用时,变量的值可能已经变了。

如果你不想被偷数据,就用默认参数把值“冻住”。这个习惯能帮你避开无数类似的坑。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个让我在GUI程序上崩溃的Bug
  • 先复现这个“灵异”现象
  • 为什么lambda会“偷”数据?
  • 这和循环的变量作用域也有关系
  • 不只是lambda,普通函数也一样
  • 怎么解决?四种方法,总有一款适合你
    • 方法1:默认参数快照(最常用)
    • 方法2:用functools.partial固定参数
    • 方法3:工厂函数(闭包套闭包)
    • 方法4:用生成器或列表推导式的“内部作用域”
  • 真实场景:GUI按钮的惨案
  • 一张表总结
  • 回到开头的Bug
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档