(第一次?)遇到了 Python 里的坑,特此记录。
Python 的 lambda 表达式是一坨〇,确实。
翻车现场
1 | l = [] |
以上代码是从业务逻辑中抽取的,已脱敏。大概的含义就是,分别把一个输出 0 的函数、输出 1 的函数、输出 2 的函数依次放到一个 list 里面。这个 list 可能是一个回调列表,或者会被遍历调用,whatever。对这个 list 里面的内容,唯一的要求就是,可以被无参数地调用(Python 里大概也算一种类型 - callable
)。
首先按照正常人的逻辑思考,这段代码预期应该输出 0。那我们来运行一下,之后神奇的事情发生了:
1 | $ python3 1.py |
我们看到,最终结果居然输出了 2。有点迷惑,但大概能想到因为l[0]
调用时应该确实有i=2
。
和魔鬼作斗争
不管是什么原因,解决问题才是第一要义。于是有了第二段代码:
1 | l = [] |
大概的思路就是,既然i
影响了我们的调用,那我们就把i
干掉。foreach
的语义,可以简单的用无返回值的map
替代(第三行这个东西是有值的,但我们没有用变量去接。它的值是[None, None, None]
)。注意map
后要自己迭代一下,例如转成list
。否则它只是一个map object
,l
也不会被append
任何对象。
去掉for
迭代变量i
的干扰后(虽然又有了一个 map func 参数x
),我们得到了所期望的结果。问题得到了初步的解决。
1 | $ python3 1.py |
注:当然这里可以写成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 | l = [] |
其实就是把i
当默认的参数绑定进去,这样就不再是延迟的绑定,而是马上绑定进这个函数里。这样也行,改动比上面那种小。但是注释还是免不了的。
具体为什么会有这个 feature 呢?尝试写了一段 C++ 代码,也许有一些道理:
1 | vector<function<void(void)>>v; |
这段代码输出结果是3,当然这是 C++ 的 for 循环导致的i=3
。可以看到 capture list 中我写的是&i
,如果去掉引用其实就不会有这个问题。不过,对于没有这种概念的 Python,其实是没得选的。
一点思考
1 | let l = [] |
1 | $ node 1.js |
不得不说,这个场景下,JavaScript 的闭包/lambda,写起来更香一点…
不过本身这个迭代加回调的场景,也确实是 JavaScript 应用更广泛一些。
既然经常在各种语言之间穿梭,那还是要对一些坑多了解点吧,不能总是搞出一些莫名其妙的问题 QAQ
之前听过一种说法,新学一门编程语言,主要学习语言特性和语法糖。现在看来,还需要加一项,就是语法坑。
以上。(?)
UPD: 你把
let
换成var
,还会这么想吗??算了,就当没有这一节内容吧。我一点也没有思考过!