KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
00 · return 不够用吗?yield 到底解决什么问题 — keel 龙骨
在学语法之前,先搞清楚它为什么存在。语法十分钟就能背下来,但不理解问题,语法永远是死知识。
在学语法之前,先搞清楚它为什么存在。语法十分钟就能背下来,但不理解问题,语法永远是死知识。
一、从一个具体问题开始:处理一个 10 GB 的日志文件
假设你要统计日志文件里 ERROR 出现的次数。文件 10 GB,内存只有 8 GB。第一版代码:
def read_all_lines(path):
with open(path) as f:
return f.readlines() # ← 一次性把 10GB 全读进内存
调用它会发生什么?MemoryError,或者机器直接卡死。问题出在 return 的语义上:return 意味着「活儿干完了,结果一次性给你」。函数要么全做完再返回,要么不做。
第二版,不用函数,直接用循环:
count = 0
with open(path) as f:
for line in f: # 文件对象本身就是惰性的:读一行,给一行
if "ERROR" in line:
count += 1
这版没问题——因为 for line in f 每次只要一行,文件对象内部按需读取。但这个「按需给」的能力被埋在文件对象里,你想复用「逐行处理」的逻辑(比如再写一个「找出含 ERROR 的行号」),就得复制粘贴循环。
你需要的是:一个可以「给一个、停一下、等着被要下一个」的函数。 这就是 yield。
def iter_error_lines(path):
with open(path) as f:
for i, line in enumerate(f):
if "ERROR" in line:
yield (i, line) # ← 给出一个,然后停在这里
# 用法 1:数数
count = sum(1 for _ in iter_error_lines(path))
# 用法 2:找行号
for i, line in iter_error_lines(path):
print(i)
同一个函数,两种消费方式,内存占用恒定为一行的大小。return 是「做完了一次性交付」,yield 是「边做边交付」。
二、第二个问题:为什么流式推送非它不可
现在把文件换成 AI 对话。模型每生成几个字就产出一个小片段(token),总共几百个片段,持续 30 秒。前端要逐字显示。
如果用 return:
def chat(): # 假想
pieces = []
for token in model_stream():
pieces.append(token)
return pieces # ← 用户要等 30 秒后才能看到第一个字
用户盯着空白页面 30 秒。而生产级代码(一次真实的 LLM 对话服务)是这样的:
async def stream_chat(...):
...
async for item in model_stream(req):
...
yield item # ← 产出一个事件,立刻交给下游
yield 让「产出」和「执行」同时发生:模型吐一个 token,函数就 yield 一个事件,SSE 就推一次,前端就闪一下字。函数执行了 30 秒,但每个片段在产出的那一刻就已经离开了函数——return 做不到这一点,因为 return 之前函数必须执行完。
三、第三个问题:它和「往列表里 append 再返回」的本质区别
你可能会说:我可以传一个回调进去嘛——process(line, callback)。或者生产者往 queue.put()、消费者 queue.get()。都能实现「边做边给」,为什么 Python 还要 yield?
| 方案 | 谁控制节奏 | 代码长在哪 | 背后的抽象 |
|---|---|---|---|
| 全量 return | 生产者(一口气做完) | 分离 | 值 |
| 回调 | 生产者(推给回调) | 控制权交给你写的函数,容易回调地狱 | 事件 |
| 队列 | 需要自己起线程/任务 | 分离,还要管并发 | 消息 |
| yield | 消费者(要一个,生产一个) | 就是一个函数,同步心智 | 序列 |
yield 的关键优势是最后一行:生产逻辑写起来是个普通函数,消费起来是个普通序列。你可以对它 for、sum、list()、切片(部分支持)、组合(下一章的 yield from)——所有处理序列的工具都能用,但它背后可能是 10 GB 文件、几百个网络事件、或者一条无限序列(itertools.count() 就是个永不结束的生成器)。
「要一个才生产一个」也带来一个独特性质:消费者不要,生产者就一步都不走。这在取消场景是天然优势——典型 SSE 生成器里,客户端断开 → request.is_disconnected() → break 跳出循环 → 生成器结束 → 上游不再生产。不需要取消协议,停止消费就是取消生产。
四、一张地图:这门课怎么走
00 为什么需要 yield(本章)
01 生成器基础:暂停/恢复的执行模型,next/for/StopIteration 逐行演算 ← 核心
02 双向通道:send/throw/close,yield from 委托
03 异步生成器:async def + yield,和 asyncio 的关系
04 三个真实场景生成器拆解
05 陷阱清单与自检
学完的检验标准:不看资料,能说清楚——
gen = f()这一行执行后,f的函数体跑了多少?(答案:零)for x in gen循环三圈,生成器函数体被执行了几次?(答案:一次,暂停恢复了两次)- 异步生成器和普通生成器最大的区别是什么?
↓下一步:01 章 · 生成器基础——把「停在半路」这件事逐行演算一遍。