KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
05 · 陷阱清单与自检 — keel 龙骨
十个真实高频的坑,每个都给「现象 → 原因 → 正确姿势」。前五个是所有生成器通用的,后五个是异步版特有的。
十个真实高频的坑,每个都给「现象 → 原因 → 正确姿势」。前五个是所有生成器通用的,后五个是异步版特有的。
一、通用陷阱(同步版就有的)
1. 生成器是一次性的
g = (x*2 for x in [1, 2, 3])
print(sum(g)) # 12
print(sum(g)) # 0 !第二次已经是空的
原因:01 章的「水流过就没了」。姿势:要复用就重新调函数生成,或先 list() 固化(确定规模小时)。
2. len() / 索引 / 打印全部不可用
g = (x for x in range(10))
print(len(g)) # TypeError
print(g[0]) # TypeError
print(g) # <generator object ...> —— 看不到内容
原因:惰性意味着「还不知道有几个、是什么」。姿势:需要这些操作就说明该用列表;或者 sum(1 for _ in g) 数个数(消费掉)。
3. 异常延迟:建生成器时不报错,消费到那行才报错
def bad():
yield 1
yield 2 / 0 # 除零
g = bad() # 不报错!
next(g) # 返回 1,不报错
next(g) # ← 这里才 ZeroDivisionError
原因:执行是惰性的,错误当然也是。工程影响:bug 可能藏在「没人消费的生成器」里悄悄不暴露。姿势:参数校验、连接建立这类「前置动作」放在生成器外层函数做,或第一次 next 前显式消费验证。
4. 循环变量迟到绑定(生成器表达式专属)
funcs = [lambda: i*2 for i in range(3)] # 列表版:lambda 闭包共享 i
print([f() for f in funcs]) # [4, 4, 4] —— i 最终是 2
gens = [(lambda: i*2) for i in range(3)] # 同样的问题
姿势:lambda i=i: i*2 用默认参数固定当前值。这条严格说是闭包坑,但总在生成器/推导式场景撞上。
5. 生成器里 return 带值,值不会从 for 里出来
def f():
yield 1
return "summary"
for x in f():
print(x) # 只打印 1,"summary" 消失了
原因:return 值挂在 StopIteration.value 上,for 循环直接丢弃。姿势:要拿返回值用 yield from(02 章)或手动 next + 捕获 StopIteration。
二、异步专属陷阱
6. await 协程和 async for 生成器混用
async def agen():
yield 1
await agen() # TypeError: object async_generator can't be used in 'await' expression
口诀(03 章判断法):函数体有 yield → async for;没有 → await。真实代码里 _execute 返回生成器所以 async for,_wait_for_run_finished 是协程所以 await——同一份代码里两种用法并存,认准函数体写法。
7. StopAsyncIteration 别在生成器内部捕获吞掉
async def wrapper(inner):
try:
async for x in inner:
yield x
except StopAsyncIteration: # 多余且危险:async for 自己会处理
pass
原因:PEP 525 规定异步生成器内部抛出的 StopAsyncIteration 会被换成 RuntimeError——你在生成器体内手动 catch/raise 它,等于破坏协议。姿势:让 async for 自己管理结束,别碰 StopAsyncIteration。
8. 忘记 aclose:带资源的异步生成器泄漏
async def with_conn():
conn = await get_connection()
try:
async for item in conn.listen():
yield item
finally:
await conn.close()
消费方 break 后如果生成器一直没被 GC 触达,finally 里的 close 就一直不执行(异步 GC 时机更不可控)。姿势:用 async with contextlib.aclosing(gen) as g: 显式管理:
import contextlib
async with contextlib.aclosing(with_conn()) as g:
async for item in g:
...
break # 退出时自动 aclose → finally 执行
9. 在 async for 里改被遍历的异步队列/流
和同步「遍历 dict 时增删 key」同类:消费速度追上生产速度时行为不可预测。姿势:生产者与消费者之间用 asyncio.Queue 解耦,遍历只针对队列。
10. 心跳/超时逻辑写死在消费者里
来自真实代码的教训:SSE 这类长连接,代理层(Nginx)的空闲超时 < 你的业务空闲间隔时,连接会被中间层悄悄掐断,你的生成器还不知道。生产级 SSE 生成器的心跳间隔 30 秒、空闲熔断 30 分钟,都是在为「链路上最不耐烦的那一环」设计。姿势:心跳间隔必须小于链路上所有超时的最小值;生成器要自查 is_disconnected,别假设框架会通知你。
三、综合自检(五题)
gen = f()之后f的函数体执行了多少?三个 next 会触发几次「函数体进入」?for x in gen中途 break,生成器最终如何收场?finally里的代码何时执行?send一个值给刚创建、从未 next 过的生成器,会发生什么?- 异步生成器里
await和yield的「暂停」有什么区别?为什么两个都需要? - 04 章例三的消费端把
async for改成列表推导,哪个功能会坏?为什么?
答案
- 零次;一次进入 + 两次恢复(01 章)。
- GC 或显式 close 时,在冻结的 yield 处抛 GeneratorExit,finally 执行;不能再 yield,否则 RuntimeError(02 章三节)。
- TypeError——必须先 next/send(None) 启动(02 章一节)。
- await 是「让出给事件循环等 IO,恢复由 IO 完成驱动」;yield 是「把值交给消费者,恢复由 anext 驱动」。生产者要等 IO 也要等消费者,所以两个都要(03 章二节)。
- 协作式取消失效——收齐列表 = 一口气跑完生成器,中间没有插入检查点的机会(04 章例三)。
✅ 五题全对,这门课就通了。接下来:Python 拔高课 讲生产代码里的高级写法与抽象。