lambda 的捕获方式决定「什么时候取值」,这一点在 C++ 和 Python 里一样。 按引用捕获配「稍后调用」,是最常见的悬空事故。
12.3.1 与 12.3.2 的示例代码里到处是 lambda。 这一节把它单独拎出来,讲清捕获这一件事。
一、C++ 的四种捕获写法
[] 什么都不捕获,[=] 按值捕获全部,[&] 按引用捕获全部,
[i] 只按值捕获 i。
四种里前三种是「范围」,第四种是「点名」。
[&] 是事故高发区:
lambda 被存起来、稍后才调用时,它引用的局部变量早就出了作用域。
这时读到的是一块已经归还的内存,结果不可预期。
编译不报错,运行时也不一定崩,最难查。
判断标准只有一句话: 这个 lambda 会不会活得比被捕获的变量久? 会,就必须按值捕获。 存进列表、交给线程、注册成回调,都属于「会活得更久」。
二、Python 里一模一样的坑
Python 的闭包默认按「名字所在的格子」取,效果等同按引用。 所以下面两行只差几个字符,结果完全不同:
fs = [lambda: i for i in range(3)] # 全是 2
fs = [lambda i=i: i for i in range(3)] # 0、1、2
第一种三个 lambda 共用同一个格子,循环结束时它已经是 2。 第二种用默认参数在「定义那一刻」就把值绑住了,所以各是各的。
这件事可以真看到:
a[0].__closure__[0].cell_contents 是 2,b[0].__defaults__ 是 0。
一个指向共用格子,一个存了当场取到的值。
这就是「按引用」与「按值」在 Python 里的实物。
三、两边对照
把这门语言对齐起来看,其实只有一句话: 「按引用」就是「晚点再取」,「按值」就是「现在就取」。
对应的写法是:
C++ 的 [&] 对 Python 的 lambda: i;
C++ 的 [=] 与 [i] 对 Python 的 lambda i=i: i。
实用建议只有一条: 凡是要把 lambda 存进列表、回调、线程里稍后再调的,一律按值捕获。 只有明确知道被引用的对象活得比自己久时,才用按引用。 这条规矩能挡掉绝大多数「偶尔才复现」的诡异 bug。
四、动手 Demo:lambda——捕获方式决定结果
同一个需求三种写法,真跑出来对比; 再把 Python 的格子与 C++ 的捕获对齐着看。
# ① 名字查找:调用时才取,全都查到同一个 i
a = [lambda: i for i in range(3)]
# ② 默认参数:定义那一刻就把值绑住
b = [lambda i=i: i for i in range(3)]
# ③ 工厂闭包:每个函数捕获自己的 i
def make(i):
def f():
return i
return f
c = [make(i) for i in range(3)]
print([f() for f in a]) # [2, 2, 2]
print([f() for f in b]) # [0, 1, 2]
print([f() for f in c]) # [0, 1, 2]
# 看到底捕获了什么
print(a[0].__closure__[0].cell_contents) # 2
print(b[0].__defaults__) # (0,)
实跑输出:
--------------------------------------------------------------------
lambda——捕获方式决定结果
--------------------------------------------------------------------
**① C++ 的 lambda 有四种捕获写法**
写法 含义 风险
------------------------------------------------------------
[] 什么都不捕获 访问外部变量直接编译失败
[=] 按值捕获全部 安全,但有拷贝开销
[&] 按引用捕获全部 最快,但对象先死就是悬空引用
[i] 按值只捕 i 最常用的写法
**`[&]` 是事故高发区:**
**lambda 被存起来、稍后才调用时,它引用的局部变量早就出了作用域。**
**这时读到的是一块已经归还的内存,结果不可预期。**
**编译不报错,运行时也不一定崩,最难查。**
**② Python 里有一模一样的坑**
**Python 的闭包默认也是「按引用」(准确地说是按名字查同一份作用域),**
**所以下面三种写法的结果完全不同。**
# ① 名字查找:全都查到同一个 i
[lambda: i for i in range(3)] → 2、2、2
# ② 默认参数:生成时就把值绑住了
[lambda i=i: i for i in range(3)] → 0、1、2
# ③ 工厂闭包
[make(i) for i in range(3)] → 0、1、2
**第一种全是 2,后两种是 0、1、2。**
**这不是 Python 的 bug,是「什么时候取这个值」的区别:**
**① 是调用时取,② ③ 是定义时取。**
**可以真看到它捕获的是什么:**
**`a[0].__closure__[0].cell_contents` 是 2,**
**因为三个 lambda 共用同一个格子,而循环结束时它已经是 2。**
**而 `b[0].__defaults__` 里存的是 0,它在定义那一刻就定死了。**
**③ 对照回 C++ 的结论**
**上面那三个 lambda 一共共用 1 个格子(一个变量一个格子)。**
C++ 写法 Python 对应 取值的时刻
-------------------------------------------------
[&] 按引用 lambda: i 调用时才取
[=] 按值 lambda i=i: i 定义时就取
[i] 指定变量 lambda i=i: i 定义时就取
**所以这门语言之间是通的:**
**「按引用」就是「晚点再取」,「按值」就是「现在就取」。**
**只要想清楚这一点,两边都不会写错。**
**最后一条实用建议:**
**凡是要把 lambda 存进列表、回调、线程里稍后再调的,一律用按值捕获。**
**只有在明确知道被引用的对象活得比自己久时,才用按引用。**
三种写法结果:全是 2、0/1/2、0/1/2。
三个 lambda 共用同一个格子,循环结束时它已经是 2。
默认参数在定义那一刻就定死了值,所以各是各的。
这和 C++ 的 [&] 与 [=] 是同一件事,只是说法不同。
30 秒实验
- 把
range(3)改成range(5),猜第一种会变成什么——全是 4,共用格子跟着走。 - 把 ② 的默认参数删掉,猜和 ① 差在哪——没差别了,都退化成名字查找。
- 在线程里用 ① 的写法,猜会出什么——读到的是线程启动后那一刻的值,不是提交时的值。
常见误区
- 「lambda 就是匿名函数,和捕获无关」——捕获方式才决定结果。
- 「Python 没有按值捕获」——有,用默认参数就是。
- 「
[&]更快所以更好」——活得更久时就是悬空引用。
自测三问(点开对答案)
1. C++ 里 `[&]` 的风险是什么? —— **按引用捕获,对象先死就成悬空引用**,不报错且难查。 2. `lambda: i` 与 `lambda i=i: i` 差在哪? —— **前者调用时才取 i,后者定义时就把值绑住。** 3. 什么情况下必须按值捕获? —— **lambda 会比被捕获的变量活得更久时**,比如存进列表或交给线程。- ☐ (本章完,下一节 13.1 WebRTC 项目简介)