(第一次?)遇到了 Python 里的坑,特此记录。

Python 的 lambda 表达式是一坨〇,确实。

翻车现场

1
2
3
4
l = []
for i in range(3):
l.append(lambda: print(i))
l[0]()

以上代码是从业务逻辑中抽取的,已脱敏。大概的含义就是,分别把一个输出 0 的函数、输出 1 的函数、输出 2 的函数依次放到一个 list 里面。这个 list 可能是一个回调列表,或者会被遍历调用,whatever。对这个 list 里面的内容,唯一的要求就是,可以被无参数地调用(Python 里大概也算一种类型 - callable)。

首先按照正常人的逻辑思考,这段代码预期应该输出 0。那我们来运行一下,之后神奇的事情发生了:

1
2
$ python3 1.py
2

我们看到,最终结果居然输出了 2。有点迷惑,但大概能想到因为l[0]调用时应该确实有i=2

和魔鬼作斗争

不管是什么原因,解决问题才是第一要义。于是有了第二段代码:

1
2
3
l = []
list(map(lambda x: l.append(lambda: print(x)), range(3)))
l[0]()

大概的思路就是,既然i影响了我们的调用,那我们就把i干掉。foreach的语义,可以简单的用无返回值的map替代(第三行这个东西是有值的,但我们没有用变量去接。它的值是[None, None, None])。注意map后要自己迭代一下,例如转成list。否则它只是一个map objectl也不会被append任何对象。

去掉for迭代变量i的干扰后(虽然又有了一个 map func 参数x),我们得到了所期望的结果。问题得到了初步的解决。

1
2
$ python3 1.py
0

注:当然这里可以写成l = list(map(lambda x: lambda: print(x), range(3))),不过我们需要首先排除append带来的影响。这里其实尝试过l = [lambda: print(i) for i in range(3)],显然并没有用。在这个问题上,列表生成器和普通循环没有什么区别。

更好的办法?

问题当然得到了解决,不过我感觉应该没人会这么搞。至少看起来就很怪。

遂 Google 之。组合了几个关键词后,大概用了这样的关键词: python lambda late binding,得到了想看的东西: Late binding closures。翻译过来就是闭包延迟绑定。Python 的 lambda 其实就是只能写一行的闭包。

一个比较可以参考的文章传送门。总而言之,这是个 feature,不是个 bug。文章里也给出了更加 common 的解决方法,虽然依旧是比较 hack 的:

1
2
3
4
l = []
for i in range(3):
l.append(lambda i=i: print(i))
l[0]()

其实就是把i当默认的参数绑定进去,这样就不再是延迟的绑定,而是马上绑定进这个函数里。这样也行,改动比上面那种小。但是注释还是免不了的。

具体为什么会有这个 feature 呢?尝试写了一段 C++ 代码,也许有一些道理:

1
2
3
4
vector<function<void(void)>>v;
for (int i=0;i<3;i++)
v.push_back([&i](){cout<<i<<endl;});
v[0]();

这段代码输出结果是3,当然这是 C++ 的 for 循环导致的i=3。可以看到 capture list 中我写的是&i,如果去掉引用其实就不会有这个问题。不过,对于没有这种概念的 Python,其实是没得选的。

一点思考

1
2
3
4
5
let l = []
for (let i = 0; i < 3; i++) {
l.push(() => console.log(i))
}
l[0]()
1
2
$ node 1.js
0

不得不说,这个场景下,JavaScript 的闭包/lambda,写起来更香一点…

不过本身这个迭代加回调的场景,也确实是 JavaScript 应用更广泛一些。

既然经常在各种语言之间穿梭,那还是要对一些坑多了解点吧,不能总是搞出一些莫名其妙的问题 QAQ

之前听过一种说法,新学一门编程语言,主要学习语言特性和语法糖。现在看来,还需要加一项,就是语法坑。

以上。(?)

UPD: 你把let换成var,还会这么想吗??

算了,就当没有这一节内容吧。一点也没有思考过!