课件 · W1 Day 3:作用域与闭包
课件 · W1 Day 3:作用域与闭包
配套代码:
../产出/实验/W1-Day3-作用域与闭包.py(7 模块 / 约 110 行) 覆盖差异条目:#9-12(索引见../产出/差异清单-C++到Python.md,本课件只引编号不重复抄) 课时:2h 跑法:python -X utf8 -W error "docs/01-Python补足/产出/实验/W1-Day3-作用域与闭包.py"本文件是本次课的完整讲解(复习材料),文字为主、表格为锚点—— 复习时没有 AI 可问,所有”你当时可能会追问的答案”都预先写在这里了。
本次课的主线一句话:
今天的坑比 Day 1-2 更反直觉——因为 C++ 的 lambda 默认行为反而是”对的”, Python 反而要手动打补丁。
模块 1 · Python 没有块作用域
猜猜看:for i in range(3): last = i 结束后,i 和 last 还存在吗?if 块里绑定的名字,块外能读到吗?
① 现象
1) for 结束后 i = 2 | last = 2
1) if 块里的名字,外面可见 = 在 if 里绑定的
for 循环跑完了,循环变量 i 和循环体内绑定的 last 都还活着,值停在最后一轮的 2;if 块里绑定的 block_var 在块外照样能读。在 C++ 直觉里,这些名字早该随着 } 销毁了——但 Python 里它们安然无恙。
② 为什么
Python 的作用域只由三种结构产生:函数(def / lambda)、类(class)、模块(每个 .py 文件)。if / for / while / try / with 只是控制流语句,不引入新作用域——写在里面的名字直接住在它们所在的那个作用域里(本例是模块全局作用域)。for 的语义是”每轮迭代把下一个值绑定给 i”,循环结束时解释器不做任何清理动作,i 就停在最后一个值上。所以”循环变量泄漏”不是 bug,是这个语言的规则。
唯一的例外是 Python 3 的推导式:[x for x in ...] 里的 x 有自己的作用域,循环结束后不泄漏到外层——这是语法里少数”像块作用域”的地方,也是为什么模块 5 用推导式和用普通 for 循环写出来的机制略有差别。
③ C++ 对照
C++ 的作用域由 {} 界定:for (int i = 0; ...) 的 i 只在循环体内可见,出了循环就没了;if (x) { int y; } 的 y 同理。把两边对齐:C++ 用 {} 划作用域,Python 用 def / class / 模块划作用域——Python 里根本没有 {} 作用域这回事。另外 C++ 的名字解析是编译期定死的,作用域是个静态规则;Python 的作用域是运行时真实存在的字典(模块 3 会验证),所以”泄漏的名字”不仅在,还能用 globals() 直接查到。
④ 坑与适用
坑:以为循环变量出了循环就死了,于是在循环外复用同名变量或以为它在别处不可见——实际拿到的是上一轮循环的残留值,而且不报错。更隐蔽的变体:循环里 try/except 绑定的异常变量 e,在 except 块结束后仍然存在(Python 3 会在结束时把它删掉,但循环里反复绑定仍容易出错)。
适用:想在循环结束后知道”最后一次处理到哪”,直接读 i 反而是个惯用法(脚本里的 last 就是这个用途)。但更稳的写法是循环前显式初始化目标变量,把”循环结束后的值”变成有意识的约定,而不是依赖泄漏这种副作用。记住:Python 里让一个名字消失只有两条路——del 它,或者它所在的函数返回。
| 结构 | C++ | Python |
|---|---|---|
for (int i = 0; ...) | i 只在循环体内可见 | i 泄漏到外层,循环结束后仍是最后值 |
if (x) { int y; } | y 出了 {} 就没了 | y 留在外层,照样能读 |
| 作用域由什么构成 | {} 与函数体 | 只有函数、类、模块 |
| 例外 | — | 推导式有自己的作用域(Python 3) |
对应条目:#9
模块 2 · 赋值即声明:UnboundLocalError
猜猜看:模块级有 count = 0,函数体里写 count += 1——能跑通吗?如果报错,是在编译时报还是运行到那一行才报?
① 现象
2) inc_bad() 抛: UnboundLocalError - cannot access local variable 'count' where it is not associated with a value
2) 异常之后 count 仍是 0
2) inc_global() 之后 count = 1
inc_bad() 抛 UnboundLocalError,模块级的 count 纹丝不动(还是 0);加上一句 global count 的 inc_global() 就能正常把 count 加到 1。错误信息值得逐字读:说的是”local variable ‘count’“——它把 count 当成了局部变量,可函数里明明没给 count 赋过值。
② 为什么
规则一句话:函数体里只要在任何位置对某个名字出现过赋值,这个名字在整个函数体内就是局部的。“赋值”的范围比直觉宽——x = ...、x += ...、for x in ...、import x、def x、with ... as x 全算。count += 1 等价于 count = count + 1:右边要先读 count,但 count 因为左边有赋值已被判定为局部名字,局部 count 还没绑定值,读它就抛 UnboundLocalError。
两个深一层的点。第一,这个”局部性判定”发生在编译函数体的时候(加载模块、生成 code object 时),不是运行到那一行才发现——所以错误信息才说得出 “local variable”:解释器早就知道它是局部的,只是还没绑定值。第二,为什么报的是 UnboundLocalError 而不是普通的 NameError:NameError 是四层作用域都查无此名;UnboundLocalError 是”已判定为局部,但尚未绑定值”——它是 NameError 的子类,给出了更精确的诊断。这个判定规则同时解释了为什么”读”全局变量不需要任何声明(没有赋值就不算局部,查找自然落到 G 层),而”写”必须显式 global。
③ C++ 对照
C++ 改外层变量直接写,没有”声明”这一步——因为 C++ 的名字解析在编译期静态完成:count++ 用的是哪个 count,看作用域规则和 :: 限定符就能确定,编译器直接编进去。Python 的 global / nonlocal 不是给编译器看的注解,而是作用域判定规则的一部分:它们的作用是阻止”函数体内出现过赋值 → 整个名字按局部处理”这条默认规则生效。换一种说法:C++ 的作用域是编译器替你算好的静态事实,Python 的作用域一部分(L/E/G 的归属)在编译期判定,但”名字到底指向谁”的查找发生在运行时——所以才会出现”编译期就判定局部、运行时才报未绑定”这种两段式的错误。
④ 坑与适用
坑:函数里顺手写 count += 1 想改全局计数器——运行到那行才炸。更阴的变体:这行代码在某个 if 分支里,而测试时分支没走到,函数一直正常返回,上线后特定路径才抛 UnboundLocalError——潜伏 bug。另一个常见误判是把它当 NameError 处理去检查”变量名拼错了没”,方向就错了:该检查的是”这个函数里是不是对它有过赋值”。
适用:global / nonlocal 能不用就不用——全局可变状态是维护噩梦,更好的路是参数传入、返回值带出、或封装成 class 的实例状态(W2 会讲 dataclass)。真正躲不开的场景是模块级配置/计数器(日志开关、统计),用 global 声明即可,但要让”谁在改全局状态”在源码里一眼 grep 得到——这正是 Python 要求显式声明的设计动机。
| 想做的事 | C++ | Python |
|---|---|---|
| 改模块级变量 | 直接 count++ | 必须写 global count |
| 只读模块级变量 | 直接读 | 直接读,不需要任何声明 |
| 报错时机 | 编译期就明确 | 运行时才发现(且取决于代码路径是否执行到) |
对应条目:#10
模块 3 · LEGB:四层查找顺序
猜猜看:模块级有 scope_x = "global",外层函数 outer 里也有 scope_x = "enclosing",inner() 读到的是哪个?
① 现象
3) inner 拿到的是: enclosing
3) builtin 层(len 无需 import): 3
inner() 返回的是 "enclosing",不是模块级的 "global"——离得近的层赢了。第二行验证 B 层的存在:len 不用 import 就能用,因为它住在内建的 builtins 层。
② 为什么
Python 查一个名字,按 L → E → G → B 的顺序由内往外逐层走,命中即停。L(Local)是当前函数的局部名字;E(Enclosing)是外层函数的局部名字——只有嵌套函数才存在这一层;G(Global)是所在模块的全局名字;B(Builtin)是解释器启动时自动注入的 builtins 模块(len、range、print 都在那里)。inner 自己的局部没有 scope_x,往外层函数 outer 的局部一查就命中了 "enclosing",查找停止——G 层的 "global" 根本没轮到。四层全落空才是 NameError。
这个”逐层字典查找”不只是比喻——Python 的作用域在运行时就是真实的数据结构:globals() 直接返回模块全局名字的字典,locals() 返回当前局部名字的字典,都能打印、能遍历。这一点让”作用域”从抽象规则变成了可内省的对象。
③ C++ 对照
C++ 也有由内向外的名字查找(局部 → 命名空间 → 全局),但有三个本质差异。其一,C++ 没有 E 层:标准 C++ 不存在嵌套函数,lambda 要用外层变量必须显式写捕获列表([&] 或 [x]),捕获了才可见;Python 的 E 层是自动的,嵌套函数读外层变量不需要任何声明。其二,解析时机:C++ 的名字绑定是编译期完成的静态事实;Python 是运行时逐层字典查找——同一个函数每次调用都可以走不同的路径(这也是晚绑定的根源之一)。其三,可内省性:C++ 的作用域规则编译完就消失了,运行时不存在”作用域对象”;Python 里 globals() / locals() 就是普通字典,作用域本身可以拿来检查。
| 层 | 内容 | C++ 对照 |
|---|---|---|
| L | 当前函数局部 | 函数体内 |
| E | 外层函数(仅嵌套函数) | C++ 没有这一层(lambda 靠显式捕获) |
| G | 模块全局 | 文件级全局 / 匿名命名空间 |
| B | 内建 | 标准库(但要 #include) |
④ 坑与适用
坑:遮蔽(shadowing)——内层定义了和外层同名的变量,你以为在读外层,实际读到的是自己的局部;更常见于 E 层和 L 层之间。诊断手段就是用 locals() / globals() 打印当前作用域看一眼。另一个坑:以为”E 层”在所有地方都存在——它只存在于嵌套函数里;模块级的两个函数之间没有 E 层,A 函数读不到 B 函数的局部变量。
适用:LEGB 是读懂一切 Python 闭包与嵌套代码的地图。读任何一段含嵌套函数的代码,先给每个变量标注它在哪一层,行为立刻清晰——模块 4 和模块 5 的所有”反直觉”,本质都是 E 层和 L 层的交互。
对应条目:#9
模块 4 · nonlocal:闭包持有的是变量本身
猜猜看:make_counter() 返回之后,按 C++ 的直觉它的栈帧已经销毁、局部变量 n 已经死了——为什么 counter() 还能把 n 一直加下去?
① 现象
4) 同一个计数器: 1 2 3
4) 另一个实例互不干扰: 1
同一个 counter 连续调用三次,返回 1、2、3——n 被记住并且持续累加。make_counter()() 临时造的新实例返回 1——两个计数器的 n 互不干扰。
② 为什么
C++ 直觉”外层函数返回 → 局部变量销毁”在这里不成立,因为 Python 的局部变量不住在栈帧里,住在 cell 对象里。编译 make_counter 时发现内层 step 引用了 n,就把 n 从普通的局部变量”提速”成一个 cell 对象;make_counter 返回时,栈帧确实没了,但 step 函数对象随身带着这个 cell 的引用一起被返回——n 活在 cell 里,由垃圾回收器管理,谁引用它谁就续命。counter() 每次执行 nonlocal n; n += 1,改的都是 cell 里那个值,所以能连续累加。
nonlocal n 这句声明的含义现在可以说清了:它告诉编译器”n 不按本函数的局部处理,按 E 层(外层函数的 cell)处理”——和模块 2 的 global 是同一套规则在 E 层的翻版。第二行输出的”互不干扰”也顺理成章:每调用一次 make_counter(),都会创建一个新的 cell,新计数器拿到的是自己的 cell,天然隔离——闭包在这里免费提供了”每实例一份状态”的语义。
③ C++ 对照
把这段代码逐字翻译成 C++——返回一个捕获了局部变量引用的 lambda——是教科书级的悬垂引用:能编译,运行时未定义行为,崩溃或读出脏值。C++ 里要安全地表达”函数返回后状态仍在”,得靠值捕获([n],拷贝一份,但从此改不到”同一个”变量)或者 std::shared_ptr 模拟 cell。Python 的 cell 由 GC 托管,无悬垂,且闭包返回后状态自然存活——这题 Python 反而比 C++ 更安全。
“读自动、写必须声明”这个不对称也是有意的设计:读外层变量是安全的、常见的,自动就好;写外层变量是会产生跨函数副作用的,强制 nonlocal / global 声明,让”谁在改外层的名字”在源码里 grep 得到——这和 C++ 社区反对隐式捕获 [&] 是同一个工程直觉。
| 需求 | C++ | Python |
|---|---|---|
| 闭包读外层变量 | 必须显式捕获 [&n] / [n] | 自动,不写也能读 |
| 闭包改外层变量 | 有 [&n] 就能直接 n++ | 必须声明 nonlocal n(E 层)/ global n(G 层) |
| 外层返回后变量存活 | [&n] → 悬垂引用,UB | cell 被闭包持有,GC 托管,无悬垂 |
| 每实例独立状态 | 靠对象封装 | 每次调用生成新 cell,天然隔离 |
④ 坑与适用
坑一:想改外层变量却忘了写 nonlocal,直接 UnboundLocalError——回到模块 2 的规则,这是它最常见的触发场景。坑二:闭包持有大对象——cell 不放,闭包不死,被捕获的大列表 / DataFrame / 连接对象就一直占着内存;排查”内存怎么降不下去”时要想到检查闭包捕获了什么(模块 6 的内省属性就是干这个的)。
适用:计数器、累加器、以及装饰器(W3 专门讲)都是 nonlocal 的正当场景。多实例状态隔离是免费送的——每个闭包自己的 cell,不用像 C++ 那样包一个 class。反过来说,如果”状态”开始变复杂(超过一两个变量、需要初始化逻辑),那就是该从闭包升级到 class 的信号。
对应条目:#10、#11
模块 5 · 晚绑定:三个 lambda 都返回 2 ★
猜猜看:funcs = [lambda: k for k in range(3)] 造出三个 lambda,[f() for f in funcs] 返回什么?直觉里”每个 lambda 记住了自己那次循环的 k”——对吗?
① 现象
5) 未修复: [2, 2, 2]
5) 用默认参数修复: [0, 1, 2]
三个 lambda,调用后全都返回 2——不是直觉中的 0、1、2。加上 lambda k=k: k 这个看起来像”复制粘贴出错”的修复,行为立刻变对。
② 为什么
关键在两件事:闭包捕获的是什么,以及什么时候取值。Python 闭包捕获的是变量本身(cell),不是值——推导式的三次迭代共用同一个变量 k(一个 cell),三个 lambda 的 __closure__ 指向的是同一个 cell;遍历结束时 cell 里装的是 2。而 lambda 体 k 的取值发生在调用那一刻(晚绑定):f() 才去 cell 里取,取到的自然是 2。三个 lambda 共享一个 cell、cell 停在 2、调用时才取——三件事叠加,[2, 2, 2] 就成了必然。
修复 lambda k=k: k 的原理要接回 Day 1-2 模块 4:默认参数在定义时求值一次。每个 lambda 在被创建的那一刻,把当时 k 的值(0、1、2)求值并拷进自己的默认参数里——调用时读的不再是共享的 cell,而是各自默认参数里冻结的快照。注意这里没有违反”捕获的是变量”的规则,而是绕开了 cell:默认参数是函数对象自带的存储(__defaults__),一 lambda 一份。
③ C++ 对照
这个坑在 C++ 默认行为下不存在,因为方向正好相反:C++ lambda 的 [k] 是值捕获——定义时把当时的值拷贝进闭包对象,每个 lambda 各持一份 0/1/2,之后外层 k 怎么变都与它们无关。所以 C++ 的默认行为就是”早绑定”,Python 的默认行为是”晚绑定(变量捕获)“——恰好互为镜像。
想共享同一个变量才需要显式写 [&k]——但那在”循环变量”场景下是新的陷阱:循环变量出了循环就销毁,持有其引用的 lambda 返回后是悬垂引用,UB。总结成一张表就是:Python 的默认(捕获变量、无悬垂)在 C++ 里要 [&k] 且不安全;C++ 的默认(值捕获)在 Python 里要 k=k 手动模拟。两边都没有免费的午餐,只是默认选项不同。
| C++ | Python | |
|---|---|---|
| 默认捕获语义 | [k] 值捕获——定义时拷一份 | 捕获变量本身(cell),调用时取值 |
| 想拿到”定义时的值” | 默认行为,不用管 | 手动模拟:lambda k=k: k |
| 想共享同一个变量 | 显式写 [&k]——循环变量场景下悬垂 UB | 默认行为,且无悬垂 |
④ 坑与适用
这是 Python 面试的头号高频题,也是真实 bug 源:批量注册回调 / 事件处理器 / 定时任务时,在循环里造 lambda 并期望它”记住当下那次循环的值”——结果所有回调全读到循环结束后的值。排查特征很典型:“明明造了 N 个不同的处理器,行为却全都一样”。
修复手段 k=k 之所以成立,靠的是 Day 1-2 模块 4 那个”默认参数定义时求值”的机制——同一个机制,Day 1-2 是 bug 的根源,Day 3 是修复的工具。用对了是快照,用错了是共享,区别只在你要的是”当时的值”还是”最新的值”。适用:明确想要所有闭包共享最新状态时(比如全部引用同一个配置对象、同一个连接池),默认行为反而是对的——不要条件反射地加 k=k,先问需求要的是快照还是别名。
补一条机制细节:Python 3 的推导式有自己的作用域(模块 1 的例外),所以这里的 k 是推导式的变量;如果改成普通 for 循环里 funcs.append(lambda: k),k 是外层函数/模块的变量——机制路径略不同,但”共享一个变量、调用时取值”的结论一样,[2, 2, 2] 照样出现。
对应条目:#11、#12
模块 6 · 把闭包拆开看:cell 与自由变量
猜猜看:add10 = make_adder(10) 之后,那个 10 存在哪里?函数对象上能找到它的痕迹吗?
① 现象
6) co_freevars = ('base',)
6) cell 内容 = [10]
6) add10(5) = 15
函数对象上直接打印出了捕获的证据:base 被列为自由变量,它所在的 cell 里装着 10,调用结果 15 验证了整条链路。
② 为什么
这三行输出把模块 4/5 讲的机制从”说法”变成了”看得见的东西”。co_freevars 是 code object 的属性,列出函数引用了、但既不在自己的参数里也不在自己的局部里的名字——这类名字叫自由变量;add10 的函数体引用了 base,所以 ('base',) 就是它的自由变量表。__closure__ 是与自由变量一一对应的 cell 对象元组——每个自由变量一个 cell,这就是变量实际住的地方;cell_contents 是 cell 当前装的值。add10(5) 执行时,函数体先从 cell 里取 base,再加 5,得 15。
“当前”两个字是重点:cell 是可变的容器,闭包持有的不是值的拷贝而是 cell 本身——外层变量后来变了、或者 nonlocal 改了它,所有持有这个 cell 的闭包同时看到新值。模块 5 的 [2, 2, 2] 就是从这里来的:三个 lambda 持有同一个 cell,读到的当然是同一个”当前值”。反过来验证:make_counter 的两个实例各有各的 cell,所以互不干扰。
③ C++ 对照
三个属性在 C++ 里都有整齐的对应物:co_freevars ≈ lambda 的捕获列表(哪些外层变量被捕获了);__closure__ ≈ 闭包对象的成员存储(捕获的变量放在哪里);cell_contents ≈ 捕获的值或引用。C++ 的闭包是编译器生成的匿名类对象,捕获的变量是它的成员——概念一一对应。
差别在于可观测性:Python 的这些属性是运行时可打印的——__closure__、co_freevars 都是普通对象属性,调试时随时看;C++ 的闭包结构藏在编译器生成的类型里,要观测得靠 std::function 的实现细节或反汇编。这也是 Python”作用域是可内省的数据结构”这个主题(模块 3)在闭包上的延续。
| 名字 | 含义 | C++ 对应 |
|---|---|---|
co_freevars | 函数引用、但不在自己局部的名字(自由变量) | lambda 的捕获列表 |
__closure__ | 每个自由变量对应一个 cell 对象 | 捕获的存储位置 |
cell.cell_contents | cell 里当前装的值 | 捕获的值 / 引用 |
④ 坑与适用
这不是新知识点,是调试工具。排查闭包 bug 时(“为什么三个都返回 2""这个 lambda 到底捕获了什么”),打印 f.__closure__ 和 f.__code__.co_freevars 一眼定位:几个 cell、谁和谁共享同一个 cell、cell 里现在是什么。模块 4 提到的”闭包持有大对象导致内存驻留”,检查手段也是它——遍历 __closure__ 看 cell_contents 里有没有意外的大对象。
进阶提示:P3 写执行循环(E 层)如果做函数级 trace,co_freevars / co_varnames / co_names 这组 code object 属性是分析”这个函数读写哪些名字”的现成入口;现在记住它们存在,到时候不用重新找。
对应条目:#11
模块 7 · def 是运行时语句:顺序即语义
猜猜看:在函数定义之前调用它,会发生什么?C++ 程序员会说”加个前向声明就行”——Python 里有对应物吗?
① 现象
7) 定义之前调用: NameError - name 'later' is not defined
7) 定义之后调用: 现在有了
定义前的调用抛 NameError,定义后的调用正常返回。同一个函数、同一个名字,差别只在调用语句写在 def 的前面还是后面。
② 为什么
def 不是声明,是一条运行时语句——和 x = 1 没有本质区别。解释器执行到 def later(): ... 这一行时,才创建函数对象、把名字 later 绑定到当前作用域;在此之前,这个名字根本不存在,调用它自然是 NameError。Python 模块没有独立的”编译 + 链接”阶段,一个 .py 文件就是一份从头执行到尾的脚本:import、赋值、def、if——全部是按顺序依次执行的语句。所谓”模块级代码的顺序敏感”,根源就在这里:整个文件是一场按序执行的计算,不是一份声明集合。
这也顺带解释了一个此前的现象:函数体内引用全局名字(比如函数体里写 print(config))时,config 在定义函数时还不需要存在——函数体的执行要等到调用那一刻,那时 config 早就绑定好了。函数体对全局名字的查找是运行时的(模块 3 的 G 层查找),所以”定义在前、数据在后”对函数体不构成问题;只有模块级直接调用才对顺序敏感。
③ C++ 对照
C++ 把声明和定义分开:头文件里放声明,源文件里放定义,链接器负责把调用点和定义对上——所以”先用后定义”靠前向声明就能编译通过,顺序可以完全解耦。Python 没有声明这回事,也没有链接阶段,def 即定义即绑定,顺序解耦的手段不存在。两边更底层的差异:C++ 的编译单元先整体编译再链接,编译器能”看到整个文件”;Python 逐条执行语句,执行到第 N 行时只”知道”前 N-1 行建立的状态。
| C++ | Python | |
|---|---|---|
| 声明与定义 | 可分离(头文件 / 前向声明) | 没有声明,只有定义 |
| 顺序要求 | 先声明即可,定义可放后面 | 模块级调用必须写在定义之后 |
| 报错时机 | 编译期 | 运行时 NameError |
④ 坑与适用
坑:把主逻辑写在文件顶部、helper 函数定义写在底部(C++ 的 main 在前、工具函数在后的习惯)——模块一跑就 NameError。同源的问题还有循环 import:模块 A 顶层 import B、模块 B 顶层又 import A,两边都在”执行到那一行”时发现对方还没准备好——解法和这里同理,把 import 挪进函数体(调用时才执行)或重构掉循环依赖。
适用:Python 的惯用法是顺应”顺序即语义”而不是对抗它——主入口放文件底部,包在 if __name__ == "__main__": 里;定义放使用点之前;需要”先用后定义”时,把使用挪进函数体(运行时才解析名字)。读第三方代码时也一样:模块顶部的执行顺序是有语义的(常量、注册表、装饰器都在 import 时生效),读文件要从上往下读,不能像读 C++ 头文件那样跳着读。
对应条目:#9
Day 3 小结
| # | 差异 | 一句话 | 最该记住的 |
|---|---|---|---|
| 9 | 没有块作用域 | 只有函数 / 类 / 模块构成作用域 | for 变量会泄漏到外层 |
| 10 | 赋值即声明 | 函数里任一处赋值 → 整个函数按局部处理 | 改外层要 global / nonlocal;报错是 UnboundLocalError 而非 NameError |
| 11 | 闭包晚绑定 | 捕获的是变量(cell),取值在调用时 | C++ 默认值捕获,Python 默认变量捕获——互为镜像 |
| 12 | 循环变量捕获 | 三个 lambda 共享一个 cell → 都是 2 | 修复靠默认参数 k=k,机制与 Day 1-2 模块 4 同源 |
本次课练习(自查题)
为什么 C++ 的 lambda 捕获没有”三个都返回 2”这个问题? 提示:对比 C++ 的
[k]与 Python 的闭包默认行为——见模块 5 的表。完整答案已写在模块 5 的 ③ 里,先自己说一遍再对。
下次课
Day 4 · 迭代协议与生成器(差异 #13-16)。 会讲”生成器只能消费一次”,以及为什么它省内存——这点在 W4 的 SSE 流式解析里直接用得上。