一文读懂Python的yield:初学者也能轻松掌握的生成器神器

一文读懂Python的yield:初学者也能轻松掌握的生成器神器

一文读懂Python的yield:初学者也能轻松掌握的生成器神器

文章目录

如果你刚学Python,可能对yield这个关键字有点陌生——它看起来像return,却又和return不一样。其实yield一点都不难,它的核心作用就一个:帮我们创建“生成器”,实现“用的时候再生成数据”,既省内存又灵活。不管是处理大文件,还是生成无限序列,yield都能派上大用场。今天我们就用最直白的话讲清yield,再配上简单代码练习,新手也能快速上手!

要搞懂yield,先对比我们最熟悉的return——毕竟它们都是“返回值”的工具,但用法和效果完全不同。下面我们先从基础例子入手,看看yield到底特殊在哪。

配合练习效果更佳哦!!

生成器函数 VS 普通函数

1、普通函数(用return):执行到return就结束,状态全销毁

# 普通函数,使用returndefnormal_func():print('执行第1步')return1print('执行第2步')# 调用执行 result = normal_func()print(f'普通函数:{result}')

这段代码运行的结果会是什么呢?

结果如下

执行第1步 普通函数:1 

2、生成器函数(用yield):遇到yield就暂停,保留状态

只要函数里有yield,它就不是普通函数了,而是“生成器函数”。调用它不会执行代码,只会得到一个“生成器对象”;只有用next()或for循环迭代时,才会执行代码

# 生成器函数,使用yielddefgen_func():print("执行第一步")yield1# 暂停执行,返回1,保留当前状态print("执行第二步")yield2# 再次暂停,返回2print("执行第三步")yield3# 最后一次暂停,返回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 0x00000238C50BA3B0> 第一次调用next(gen): 执行第一步 1 第二次调用next(gen): 执行第二步 2 第三次调用next(gen): 执行第三步 3

3、用for循环迭代生成器

手动写next()太麻烦,for循环会自动处理StopIteration异常,迭代起来更简单,这也是实际开发中最常用的方式

defgen_func():print("执行第一步")yield1# 暂停执行,返回1,保留当前状态print("执行第二步")yield2# 再次暂停,返回2print("执行第三步")yield3# 最后一次暂停,返回3print("for循环迭代生成器:")for num in gen_func():print("获取到的值:", num)

可以得到结果如下:

for循环迭代生成器: 执行第一步 获取到的值: 1 执行第二步 获取到的值: 2 执行第三步 获取到的值: 3

通过以上代码的练习,我们可以发现:

return是“一次性返回,直接结束”,yield是“分次返回,暂停保留状态”——这就是yield最核心的特点。

核心区别

特性return(普通函数)yield(生成器函数)
执行逻辑执行到 return 立即终止函数,销毁状态执行到 yield 暂停函数,保留当前状态
返回值直接返回最终值,函数调用即执行返回生成器对象,迭代时才逐步返回值
内存占用一次性生成所有数据,占用内存大按需生成数据,仅占用当前迭代的内存
可迭代性无(返回单个 / 多个值,需手动封装迭代)生成器对象本身是可迭代对象,支持 for/next

yield的核心优势:惰性求值

这个时候,问题很多的小明就要问了:“既然return也能返回值,我为啥还要用yield呀?”

答曰:yield能省内存!这种“用的时候再生成数据”的方式,叫“惰性求值”

为啥用yield就可以省内存呀?

假如说,现在我们需要100w条数据,用list存储,return返回,他会一次性把100w的数据全部放到内存中,这个时候,就有可能会导致电脑卡顿;但是如果使用yield,yield每次只会返回1个数据,用完就扔,可以说是几乎不占内存。

现在我们写一段代码来实验一下:

import time import sys defcreate_big_list():print("开始创建列表...") result =[i for i inrange(1000000)]print("列表创建完成")return result defcreate_big_gen():print("创建生成器对象...")for i inrange(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 inrange(5):print(f" 第{i+1}个值: {next(gen_big)}")

结果如下:

=== 创建阶段 === 开始创建列表... 列表创建完成 创建列表耗时: 0.043654秒 创建生成器耗时: 0.000000秒 === 内存使用 === 列表大小: 8,448,728 字节 生成器大小: 104 字节 === 惰性求值验证 === 从生成器获取前5个值: 创建生成器对象... 第1个值: 0 第2个值: 1 第3个值: 2 第4个值: 3 第5个值: 4

可以看出yield消耗的内存可以说是远小于直接使用return返回的消耗的

另一个实用场景:逐行读取大文件。如果直接用read()读取几十GB的日志文件,会瞬间占满内存;用yield逐行读,就不会有这个问题

defread_big_file(file_path):# 打开文件(with语句会自动关闭文件,新手放心用)withopen(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():给生成器“传值”(双向通信)

yield不仅能返回值,还能接收外部传进来的值,用send()方法就行。注意:第一次传值前,要先用next()触发生成器到暂停状态。

defchat_gen():print("生成器:你好!请给我发一条消息~") msg1 =yield"等待你的消息..."# 暂停,返回提示语,同时接收外部传值print(f"生成器:收到你的消息啦:{msg1}") msg2 =yieldf"已确认消息:{msg1}"# 再次暂停,接收第二条消息print(f"生成器:又收到一条消息:{msg2}")yieldf"结束对话,共收到两条消息"# 测试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:简化嵌套生成器

如果有嵌套的生成器(生成器里套生成器),用yield from能直接迭代内部生成器的值,不用写复杂循环。

# 子生成器(内部的小生成器)defsub_gen():yield"苹果"yield"香蕉"# 主生成器(外部的生成器)defmain_gen():yield"开始输出水果:"yieldfrom sub_gen()# 直接迭代sub_gen()的所有值,等价于for val in sub_gen(): yield valyield"结束输出水果"# 迭代主生成器for val in main_gen():print(val)

可以得到如下结果:

开始输出水果: 苹果 香蕉 结束输出水果 

yield常见应用场景

常见的应用场景如下:

  • 处理大文件/大数据:比如逐行读日志、生成百万级数据(省内存);
  • 生成无限序列:比如生成自然数、斐波那契数列(列表存不下,生成器能一直给值);
  • 分步执行任务:比如爬虫的“请求网页→解析数据→保存数据”,每一步用yield暂停,方便调试;
  • 简单协程/异步:入门级的异步操作(比如简单的任务调度),yield是基础。

最后再实现一个用yield生成无限斐波那契数列的代码:

斐波那契数列:在一组数据中,每个数都等于前两个数之和

deffib_gen(): a, b =0,1whileTrue:# 无限循环,生成器不会一次性执行完yield a # 每次返回一个斐波那契数 a, b = b, a + b # 更新值# 取前10个斐波那契数(避免无限迭代) fib = fib_gen()print("前10个斐波那契数:")for _ inrange(10):print(next(fib), end=" ")

总结:新手掌握yield的核心要点

其实yield一点都不复杂,新手记住3个核心点就行:

  1. 有yield的函数是生成器函数,调用不执行,返回生成器对象;
  2. 用next()或for循环触发执行,遇到yield就暂停、返回值、保留状态;
  3. 核心优势是惰性求值,省内存,适合大数据/大文件场景。

Read more

【优选算法必刷100题】第001~002题(双指针算法):移动零、复写零问题求解

【优选算法必刷100题】第001~002题(双指针算法):移动零、复写零问题求解

🔥个人主页:艾莉丝努力练剑 ❄专栏传送门:《C语言》、《数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题、洛谷刷题、C/C++基础知识知识强化补充、C/C++干货分享&学习过程记录、测试开发要点全知道、Linux操作系统编程详解、笔试/面试常见算法:从基础到进阶 🍉学习方向:C/C++方向学习者 ⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平 目录 001  移动零 1.1  思路 1.2  算法原理 1.3  代码实现   1.4  过程推算 002  复写零 2.1  思路

By Ne0inhk
【C++STL上】栈和队列模拟实现 容器适配器 力扣经典算法秘籍

【C++STL上】栈和队列模拟实现 容器适配器 力扣经典算法秘籍

🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C++ 🐶学习方向:C++方向学习爱好者 ⭐人生格言:得知坦然 ,失之淡然 🏠博主简介 文章目录 * 前言 * 一、栈与队列原型简介 * 1.1 Stack * 1.2 Queue * 1.3 最小栈的练习 * 1.4 栈的压入、弹出序列 * 1.5 二叉树的层序遍历 * 二、模拟实现栈 * 2.1 容器适配器 * 2.2栈的实现 * 三、模拟实现队列 * 三、总结 前言 本文从STL容器适配器视角,深度解析栈与队列的设计本质——以双端队列(deque)为底层容器,实现高效头尾操作。

By Ne0inhk
【优选算法必刷100题】第009~010题(滑动窗口):长度最小的子数串、无重复字符的最长字串

【优选算法必刷100题】第009~010题(滑动窗口):长度最小的子数串、无重复字符的最长字串

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》《优选算法指南-必刷经典100题》 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 09.长度最小的子数串 解法一:(暴力求解)(会超时) 算法思路: 解法二:(滑动窗口) 算法思路: C++代码演示: 算法总结&&笔记展示: 10.无重复字符的最长字串 解法一:(暴力求解)(不会超时,可以通过): 算法思路: 解法二:(滑动窗口) 算法思路: C++代码演示: 算法总结&&笔记展示: 09.长度最小的子数串 题目链接: 209. 长度最小的子数组 -

By Ne0inhk
【优选算法必刷100题:专题六】(模拟算法)第039~343题:替换所有的问号、提莫攻击、Z 字形变换、外观数列、数青蛙

【优选算法必刷100题:专题六】(模拟算法)第039~343题:替换所有的问号、提莫攻击、Z 字形变换、外观数列、数青蛙

🎬 个人主页:艾莉丝努力练剑 ❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》 《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬 艾莉丝的简介: 🎬艾莉丝的算法专栏简介: 文章目录 * 039 替换所有的问号 * 1.1 解法:模拟的思想 * 1.2 算法实现 * 1.3 博主手记 * 040 提莫攻击 * 2.1 解法:模拟 + 分情况讨论 * 2.2 算法实现 * 2.3 博主手记 * 041 Z 字形变换

By Ne0inhk