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 陷阱清单与自检

学完的检验标准:不看资料,能说清楚——

  1. gen = f() 这一行执行后,f 的函数体跑了多少?(答案:零)
  2. for x in gen 循环三圈,生成器函数体被执行了几次?(答案:一次,暂停恢复了两次)
  3. 异步生成器和普通生成器最大的区别是什么?

↓下一步:01 章 · 生成器基础——把「停在半路」这件事逐行演算一遍。

进入 keel 阅读