前言
旁边的实习生非常苦恼的问:我每次遇到yield都会困惑,看一次相关博客就感觉好像懂了,但是时间一长又完全记不清了,它不像方法中的return很常用,知道就是用来结束方法,并返回值的。
我:那说明你每次看完博客,认为自己理解了,其实依然处于一知半解,这里需要你有思维上的一个转变。我们接下来,就用通俗易懂的案例帮你一次性彻底理解。
一句话概括
yield 是用来造"生成器"的,它让函数变成一个"懒惰的工厂",只在你需要的时候,才生产一个产品,而不是一口气全做好。
先回忆一下 return
python
def get_numbers():
result = []
for i in range(5):
result.append(i)
return result # 一次性返回所有结果
nums = get_numbers()
print(nums) # [0, 1, 2, 3, 4]
这个函数像一个勤劳的厨师:你点单后,他先把5个菜全做完,装在一个盘子里,然后一次性端给你。
但如果要生成 100 万个数字呢?他得先把 100 万个数字全做好,再给你 ------ 这很浪费内存。
现在,yield 上场了
python
def get_numbers_yield():
for i in range(5):
yield i # 每次只给你一个,不一口气全做
gen = get_numbers_yield()
print(gen) # <generator object get_numbers_yield at 0x...>
这时候你拿到的 gen 不是一个列表,而是一个"生成器对象"------ 它像是一个懒惰的厨师,你不说"下一个",他就不做菜。
你什么时候说"下一个"?用 next():
python
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 2
或者用 for 循环(它自动帮你调用 next):
python
for num in get_numbers_yield():
print(num) # 依次打印 0, 1, 2, 3, 4
yield 的关键点:暂停和恢复
yield 最神奇的地方是:函数执行到 yield 时会"暂停",记住当前状态,下次调用时从暂停的地方继续。
我们加点打印来看:
python
def my_generator():
print("Step 1")
yield "A"
print("Step 2")
yield "B"
print("Step 3")
yield "C"
gen = my_generator()
print("第一次调用:")
print(next(gen)) # 打印 Step 1,然后返回 "A"
print("第二次调用:")
print(next(gen)) # 打印 Step 2,然后返回 "B"
print("第三次调用:")
print(next(gen)) # 打印 Step 3,然后返回 "C"
看到了吗?函数不是一次性跑完的,而是一步步走,走一步停一下,等你叫它才继续。
return vs yield 对比
特性 | return | yield |
---|---|---|
返回值 | 一次性返回所有结果 | 每次返回一个,可以多次 |
函数状态 | 返回后函数结束,状态丢失 | 暂停,记住状态,下次继续 |
内存占用 | 高(全存下来) | 低(只生成一个) |
返回类型 | 通常是列表等 | 生成器对象(可迭代) |
使用场景 | 结果小,需要全部使用 | 大数据、流式处理、节省内存 |
举个生活例子:流水线工人
想象你在工厂流水线上:
- return:工人把1000个零件全组装好,堆成一堆给你。
- yield:工人每次只组装一个,放到传送带上,你拿一个,他再做下一个。
如果零件很大,堆一堆会占满仓库(内存爆炸)------ 所以 yield 更"聪明"。
通俗易懂的代码示例
🌟 场景 1:处理大数据,节省内存(最常用!)
问题:你想生成 1 亿个数字,但内存不够。
解决:用 yield 按需生成,不占内存。
python
# bad:用 return + list ------ 内存爆炸!
def big_list():
result = []
for i in range(1000000): # 100万已经很大了
result.append(i)
return result
# good:用 yield ------ 内存几乎为0
def big_generator():
for i in range(1000000):
yield i
# 测试
gen = big_generator()
# 只取前5个
for i, num in enumerate(gen):
if i >= 5:
break
print(num) # 输出 0, 1, 2, 3, 4
✅ 适用:读大文件、数据库大批量数据、日志处理等。
🌟 场景 2:逐行读取大文件(经典案例)
问题:一个 10GB 的日志文件,不能一次性读进内存。
解决:用 yield 一行一行读。
python
def read_large_file(filename):
with open(filename, 'w') as f: # 先创建测试文件
for i in range(10):
f.write(f"日志行 {i}: 用户登录\n")
# 真正的读取函数
with open(filename, 'r') as f:
for line in f:
yield line.strip()
# 测试
for log in read_large_file('test.log'):
print("处理:", log)
运行后你会看到:
python
处理: 日志行 0: 用户登录
处理: 日志行 1: 用户登录
✅ 适用:任何大文件处理。
🌟 场景 3:无限序列生成器
问题:你想生成无限的斐波那契数列、自然数等。
解决:yield 可以"无限产出"。
python
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 只取前10个
fib = fibonacci()
for _ in range(10):
print(next(fib), end=' ') # 0 1 1 2 3 5 8 13 21 34
✅ 适用:数学序列、ID 生成、游戏资源生成等。
🌟 场景 4:模拟状态机 / 分步流程
问题:你想实现一个多步骤的流程,比如"登录 → 验证 → 成功"。
解决:用 yield 分步返回。
python
def login_flow():
yield "步骤1: 请输入用户名"
username = yield "步骤2: 请输入密码"
if username == "admin":
yield "步骤3: 登录成功!"
else:
yield "步骤3: 用户名错误!"
# 测试
flow = login_flow()
print(next(flow)) # 步骤1
print(next(flow)) # 步骤2
print(next(flow)) # 步骤3(成功或失败)
输出:
python
步骤1: 请输入用户名
步骤2: 请输入密码
步骤3: 登录成功!
✅ 适用:向导流程、游戏关卡、审批流程等。
🌟 场景 5:合并多个数据源(管道)
问题:你想从多个地方读数据,像流水线一样处理。
解决:用 yield from 或多个生成器串联。
python
def source1():
yield "A"
yield "B"
def source2():
yield "X"
yield "Y"
def combined():
yield from source1() # 把 source1 的所有 yield 拿过来
yield from source2()
# 测试
for item in combined():
print(item) # A, B, X, Y
✅ 适用:数据聚合、ETL(抽取-转换-加载)、日志合并等。
🌟 场景 6:实时数据推送(Web 流)
问题:你想在网页上实时显示时间、股票价格等。
解决:用 yield + StreamingResponse(FastAPI 示例)
python
# 安装:pip install fastapi uvicorn
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import time
app = FastAPI()
def generate_time_stream():
for _ in range(5):
yield f"data: 当前时间: {time.strftime('%H:%M:%S')}\n\n"
time.sleep(1)
@app.get("/time")
async def time_stream():
return StreamingResponse(
generate_time_stream(),
media_type="text/event-stream"
)
# 运行:uvicorn script_name:app --reload
# 然后浏览器访问 http://localhost:8000/time
你会看到时间一秒一秒地更新!
✅ 适用:SSE(Server-Sent Events)、实时监控、聊天室等。
🌟 场景 7:协程 / 生成器通信(高级用法)
问题:你想让生成器接收外部数据。
解决:用 send() 方法。
python
def accumulator():
total = 0
while True:
value = yield total # yield 返回当前 total,并接收新值
if value is not None:
total += value
# 测试
acc = accumulator()
print(next(acc)) # 0(启动生成器)
print(acc.send(10)) # 10
print(acc.send(5)) # 15
print(acc.send(-3)) # 12
✅ 适用:状态维护、计算器、游戏得分系统等。
7 种消费生成器的方法
✅ 核心前提:生成器(generator)是一个迭代器(iterator),所以所有能处理"可迭代对象"或"迭代器"的方式,都可以用来消费生成器。
方法 | 说明 | 是否推荐 | 示例 |
---|---|---|---|
for循环 | 最常用、最直观 | ⭐⭐⭐⭐⭐ | for |
next()函数 | 手动控制,一次一个 | ⭐⭐⭐⭐☆ | next(gen) |
内置函数:list()/tuple()/set() | 一次性转成容器 | ⭐⭐⭐☆☆ | list(gen) |
解包操作* | 展开成参数或列表 | ⭐⭐⭐☆☆ | [*gen] |
传递给需要迭代器的函数 | 如sum(),max(),any()等 | ⭐⭐⭐⭐☆ | sum(gen) |
作为参数传给函数(如print(*gen)) | 结合解包使用 | ⭐⭐⭐☆☆ | print(*gen) |
用于流式响应(如StreamingResponse) | Web中的SSE、文件下载等 | ⭐⭐⭐⭐⭐ | return StreamingResponse(gen) |