Python 的 yield 关键字详解:生成器函数与惰性求值
Python 中的 yield 关键字。对比了普通函数(return)与生成器函数(yield)的区别,指出 yield 具有惰性求值特性,能显著降低内存占用,适合处理大数据或大文件。介绍了 send() 方法实现双向通信及 yield from 简化嵌套生成器的用法。通过代码示例展示了斐波那契数列生成、大列表创建对比等场景,帮助开发者理解如何高效利用生成器优化程序性能。

Python 中的 yield 关键字。对比了普通函数(return)与生成器函数(yield)的区别,指出 yield 具有惰性求值特性,能显著降低内存占用,适合处理大数据或大文件。介绍了 send() 方法实现双向通信及 yield from 简化嵌套生成器的用法。通过代码示例展示了斐波那契数列生成、大列表创建对比等场景,帮助开发者理解如何高效利用生成器优化程序性能。

如果你刚学 Python,可能对 yield 这个关键字有点陌生——它看起来像 return,却又和 return 不一样。其实 yield 一点都不难,它的核心作用就一个:帮我们创建'生成器',实现'用的时候再生成数据',既省内存又灵活。不管是处理大文件,还是生成无限序列,yield 都能派上大用场。
要搞懂 yield,先对比我们最熟悉的 return——毕竟它们都是'返回值'的工具,但用法和效果完全不同。
return 就结束,状态全销毁def normal_func():
print('执行第 1 步')
return 1
print('执行第 2 步')
# 调用执行
result = normal_func()
print(f'普通函数:{result}')
这段代码运行的结果会是什么呢?
结果如下:
执行第 1 步 普通函数:1
yield 就暂停,保留状态只要函数里有 yield,它就不是普通函数了,而是'生成器函数'。调用它不会执行代码,只会得到一个'生成器对象';只有用 next() 或 for 循环迭代时,才会执行代码。
def gen_func():
print("执行第一步")
yield 1 # 暂停执行,返回 1,保留当前状态
print("执行第二步")
yield 2 # 再次暂停,返回 2
print("执行第三步")
yield 3 # 最后一次暂停,返回 3
# 调用生成器函数,不会执行代码,只得到生成器对象
gen = gen_func()
print("直接调用的结果:", gen)
# 用 next() 触发执行(每次 next(),执行到下一个 yield 就停)
print("\n第一次调用 next(gen):")
print(next(gen))
print("\n第二次调用 next(gen):")
print(next(gen))
print("\n第三次调用 next(gen):")
print(next(gen))
# print(next(gen)) # 会抛出 StopIteration 异常!
对应结果如下:
如果第四次调用 next(gen):生成器耗尽,会抛 StopIteration 异常。
直接调用的结果:<generator object gen_func at 0x...>
第一次调用 next(gen):
执行第一步
1
第二次调用 next(gen):
执行第二步
2
第三次调用 next(gen):
执行第三步
3
手动写 next() 太麻烦,for 循环会自动处理 StopIteration 异常,迭代起来更简单,这也是实际开发中最常用的方式。
def gen_func():
print("执行第一步")
yield 1
print("执行第二步")
yield 2
print("执行第三步")
yield 3
print("for 循环迭代生成器:")
for num in gen_func():
print("获取到的值:", num)
可以得到结果如下:
for 循环迭代生成器:
执行第一步
获取到的值:1
执行第二步
获取到的值:2
执行第三步
获取到的值:3
通过以上代码的练习,我们可以发现:return 是'一次性返回,直接结束',yield 是'分次返回,暂停保留状态'——这就是 yield 最核心的特点。
| 特性 | return(普通函数) | yield(生成器函数) |
|---|---|---|
| 执行逻辑 | 执行到 return 立即终止函数,销毁状态 | 执行到 yield 暂停函数,保留当前状态 |
| 返回值 | 直接返回最终值,函数调用即执行 | 返回生成器对象,迭代时才逐步返回值 |
| 内存占用 | 一次性生成所有数据,占用内存大 | 按需生成数据,仅占用当前迭代的内存 |
| 可迭代性 | 无(返回单个 / 多个值,需手动封装迭代) | 生成器对象本身是可迭代对象,支持 for/next |
既然 return 也能返回值,为何还要用 yield?
答曰:yield 能省内存!这种'用的时候再生成数据'的方式,叫'惰性求值'。
假如说,现在我们需要 100w 条数据,用 list 存储,return 返回,它会一次性把 100w 的数据全部放到内存中,这个时候,就有可能会导致电脑卡顿;但是如果使用 yield,yield 每次只会返回 1 个数据,用完就扔,可以说是几乎不占内存。
现在我们写一段代码来实验一下:
import time
import sys
def create_big_list():
print("开始创建列表...")
result = [i for i in range(1000000)]
print("列表创建完成")
return result
def create_big_gen():
print("创建生成器对象...")
for i in range(1000000):
yield i
print("生成器完成所有值的生成")
# 对比创建时间
print("=== 创建阶段 ===")
start = time.time()
list_big = create_big_list()
print(f"创建列表耗时:{time.time()- start:.6f}秒")
start = time.time()
gen_big = create_big_gen()
print(f"创建生成器耗时:{time.time()- start:.6f}秒")
# 对比内存使用
print(f"\n=== 内存使用 ===")
print(f"列表大小:{sys.getsizeof(list_big):,} 字节")
print(f"生成器大小:{sys.getsizeof(gen_big):,} 字节")
# 验证惰性取值
print("\n=== 惰性求值验证 ===")
print("从生成器获取前 5 个值:")
for i in range():
()
结果如下:
=== 创建阶段 ===
开始创建列表...
列表创建完成
创建列表耗时:0.043654 秒
创建生成器耗时:0.000000 秒
=== 内存使用 ===
列表大小:8,448,728 字节
生成器大小:104 字节
=== 惰性求值验证 ===
从生成器获取前 5 个值:
创建生成器对象...
第 1 个值:0
第 2 个值:1
第 3 个值:2
第 4 个值:3
第 5 个值:4
可以看出 yield 消耗的内存可以说是远小于直接使用 return 返回的消耗的。
另一个实用场景:逐行读取大文件。如果直接用 read() 读取几十 GB 的日志文件,会瞬间占满内存;用 yield 逐行读,就不会有这个问题。
def read_big_file(file_path):
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
yield line.strip()
这里可以把内存看作家里的冰箱,而 return 与 yield 的区别就在于:
return 会一次性买一周的量,可能会有冰箱装不下的风险。yield 则是每次只买做一顿饭的量,吃多少买多少,所以冰箱不会有爆满的风险。普通函数执行流程: 调用函数 → 执行代码 → 遇到 return → 返回值 → 函数结束
生成器函数执行流程: 调用函数 → 返回生成器对象 → next() 触发 → 执行到 yield 暂停 → 返回值 → 保留状态 → 下次 next() → 从暂停处继续 → …
yield 不仅能返回值,还能接收外部传进来的值,用 send() 方法就行。注意:第一次传值前,要先用 next() 触发生成器到暂停状态。
def chat_gen():
print("生成器:你好!请给我发一条消息~")
msg1 = yield "等待你的消息..." # 暂停,返回提示语,同时接收外部传值
print(f"生成器:收到你的消息啦:{msg1}")
msg2 = yield f"已确认消息:{msg1}" # 再次暂停,接收第二条消息
print(f"生成器:又收到一条消息:{msg2}")
yield f"结束对话,共收到两条消息"
# 测试 send() 用法
gen_chat = chat_gen()
# 第一步:用 next() 触发生成器到第一个 yield
first_reply = next(gen_chat)
print("我收到的回复:", first_reply)
# 第二步:用 send() 传值,同时触发生成器继续执行
second_reply = gen_chat.send("Hello! yield 真有趣~")
print("我收到的回复:", second_reply)
# 第三步:再传一条消息
third_reply = gen_chat.send("我学会啦!")
print("我收到的回复:", third_reply)
结果如下:
生成器:你好!请给我发一条消息~
我收到的回复:等待你的消息...
生成器:收到你的消息啦:Hello! yield 真有趣~
我收到的回复:已确认消息:Hello! yield 真有趣~
生成器:又收到一条消息:我学会啦!
我收到的回复:结束对话,共收到两条消息
如果有嵌套的生成器(生成器里套生成器),用 yield from 能直接迭代内部生成器的值,不用写复杂循环。
# 子生成器(内部的小生成器)
def sub_gen():
yield "苹果"
yield "香蕉"
# 主生成器(外部的生成器)
def main_gen():
yield "开始输出水果:"
yield from sub_gen() # 直接迭代 sub_gen() 的所有值,等价于 for val in sub_gen(): yield val
yield "结束输出水果"
# 迭代主生成器
for val in main_gen():
print(val)
可以得到如下结果:
开始输出水果:
苹果
香蕉
结束输出水果
常见的应用场景如下:
yield 暂停,方便调试;yield 是基础。最后再实现一个用 yield 生成无限斐波那契数列的代码:
斐波那契数列:在一组数据中,每个数都等于前两个数之和
def fib_gen():
a, b = 0, 1
while True: # 无限循环,生成器不会一次性执行完
yield a # 每次返回一个斐波那契数
a, b = b, a + b # 更新值
# 取前 10 个斐波那契数(避免无限迭代)
fib = fib_gen()
print("前 10 个斐波那契数:")
for _ in range(10):
print(next(fib), end=" ")
总结:新手掌握 yield 的核心要点
其实 yield 一点都不复杂,新手记住 3 个核心点就行:
yield 的函数是生成器函数,调用不执行,返回生成器对象;next() 或 for 循环触发执行,遇到 yield 就暂停、返回值、保留状态;
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online