Python的生成器把我坑惨了,原来yield和return的区别这么大

简介: 本文以一次日志分析导致服务器内存爆表的实战经历为引,深入浅出对比 Python 中 `return` 与 `yield` 的本质区别:前者“一次性交作业”,后者“边做边上菜”。通过食堂打饭等生动比喻,详解生成器的内存优势、使用场景及常见陷阱,助你高效处理大数据。

一个让服务器差点崩了的Bug

去年公司接了一个日志分析的项目。每天产生几十GB的Nginx日志,需要统计每个IP的访问次数和状态码分布。

我当时的方案很简单:

def load_logs(file_path):
   with open(file_path, 'r') as f:
       lines = f.readlines()
   return lines

logs = load_logs("access.log")
for line in logs:
   parse_and_count(line)

代码跑起来之后,服务器内存直接飙到95%。几十GB的日志文件,readlines()一次性全部读到内存里——我的16GB服务器根本扛不住。

同事跑过来看了一眼:“你没用生成器?”

我一脸茫然:“生成器是什么?”

那天下午,我把yield和return的区别彻底研究了一遍。今天把这些东西讲清楚,希望你下次遇到类似问题别像我一样慌。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (46).png

第一步:return——一次性交作业

先搞清楚return是怎么回事。这玩意儿几乎所有编程语言都有,大家都很熟。

def get_numbers():
   result = []
   for i in range(5):
       result.append(i)
   return result

nums = get_numbers()
print(nums)  # [0, 1, 2, 3, 4]

return的逻辑很简单:函数执行到return,把所有结果打包好,一次性交给你,然后函数结束。

打个比方:你去食堂打饭。return版本的食堂师傅是这样工作的——你先排队等着,师傅把100份盒饭全部做完,码整齐了,然后一次性端出来给你。你才能开始吃。

这个模式有个问题:如果要做10000份盒饭,你得等很久,而且师傅一次性端出来需要很大的台面(内存)来放。

体现在代码上就是:return必须先把所有结果存到一个中间变量里(比如上面代码里的result列表),等全部算完了再返回。数据量小的时候没问题,数据量大了——就像我那几十GB的日志——内存直接爆炸。

第二步:yield——边做边上菜

yield就不一样了。

def get_numbers():
   for i in range(5):
       yield i

nums = get_numbers()
print(next(nums))  # 0
print(next(nums))  # 1
print(next(nums))  # 2

第一次调用next(nums),函数开始执行,遇到yield i就停下来,把i返回给你。第二次调用next(nums),函数从上次停下的地方继续执行,而不是从头开始。

还是食堂打饭的比喻:yield版本的师傅是这样工作的——你做一份盒饭,我吃一份。师傅不用把所有盒饭都做完才给我,我不用等。

体现在代码上:yield不需要一个中间列表来存所有结果,它每次只生成一个值,用完就扔。

回到我那个日志文件的例子,用yield改造一下:

def load_logs(file_path):
   with open(file_path, 'r') as f:
       for line in f:          # 文件对象本身支持逐行迭代
           yield line.strip()  # 每次只返回一行

for line in load_logs("access.log"):
   parse_and_count(line)       # 逐行处理,内存里永远只有一行

改完之后,内存占用从95%降到了不到5%。因为生成器不会把整个文件读到内存里,而是每次只读一行,处理完再读下一行。

第三步:核心区别——暂停 vs 结束

return和yield最本质的区别就一句话:

return结束函数,yield暂停函数。

看这段代码:

def demo_return():
   print("第1步")
   return "结果1"
   print("第2步")  # 永远不会执行
   return "结果2"  # 永远不会执行

def demo_yield():
   print("第1步")
   yield "结果1"
   print("第2步")
   yield "结果2"
   print("第3步")

调用demo_return(),打印“第1步”,返回“结果1”,函数结束。后面的代码全都不执行。

调用demo_yield(),返回的是一个生成器对象,函数体本身不会立即执行。只有当你调用next()的时候,它才会开始跑:

gen = demo_yield()
print(next(gen))  # 打印"第1步",返回"结果1",暂停
print(next(gen))  # 打印"第2步",返回"结果2",暂停
print(next(gen))  # 打印"第3步",然后函数结束,抛出StopIteration

yield把函数的执行变成了可暂停、可恢复的。每次yield都保存了当前的所有状态——局部变量的值、执行到了哪一行——下次继续的时候全部恢复。

第四步:yield和return能不能一起用?

能。但有坑。

在生成器函数里用return,它的作用是终止生成器,而不是返回一个值给调用者。

def my_generator():
   yield 1
   yield 2
   return "结束了"  # 这个值不会直接返回给调用者
   yield 3          # 这行永远不会执行

gen = my_generator()
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 抛出 StopIteration: 结束了

在Python 3.3及以上版本中,生成器里的return返回值会被包装进StopIteration异常里。你可以通过捕获异常来拿到这个值,但正常迭代的时候是拿不到的。

所以我的建议是:别在生成器里混用return,除非你明确知道自己在做什么。想让生成器终止,让函数自然结束就好了。

第五步:什么时候该用yield?

搞清楚区别之后,关键是知道什么时候用哪个。

用return的场景:

  • 数据量小,一次性加载没问题
  • 你需要立即拿到完整的处理结果
  • 函数逻辑简单,不需要维护状态

用yield的场景:

  • 处理大文件:几十GB的日志、CSV、JSON文件,逐行处理
  • 处理无限序列:比如斐波那契数列、实时数据流,你不知道什么时候结束
  • 数据流式处理:从数据库批量读取、从API分页获取数据
  • 需要节省内存:生成器不管产生多少值,占用的内存几乎不变

有个数据可以直观感受一下:生成100万个平方数,用列表存需要大约8MB内存,用生成器只需要大约112字节——差距是几万倍。

第六步:还有几个坑

坑一:生成器只能迭代一次

gen = (x for x in range(5))
print(list(gen))  # [0, 1, 2, 3, 4]
print(list(gen))  # [] —— 空了!

生成器像是一个单向的迭代器,用完就没了。想再用就得重新创建。

坑二:生成器不是快,是省内存

很多人以为生成器比普通函数快。实际上恰恰相反——由于每次yield都要暂停和恢复上下文,生成器通常比普通函数慢一点。

生成器的优势是内存效率,不是执行速度。在处理大数据的时候,内存效率比速度重要得多——内存爆了程序直接崩,慢一点至少还能跑完。

坑三:生成器表达式和列表推导式长得像

# 列表推导式——立即生成所有数据
squares_list = [x**2 for x in range(10)]  # 方括号

# 生成器表达式——惰性求值
squares_gen = (x**2 for x in range(10))   # 圆括号

两者只差一个括号,但行为天差地别。用错了内存直接翻车。

总结

一句话记住区别:**return是“做完了一起给”,yield是“做一点给一点”**。

  • return:一次性返回,函数结束,占用内存大
  • yield:逐个返回值,函数暂停,占用内存小

用我那个日志分析的例子来总结:

  • **用return**:把整本《战争与和平》从头到尾抄一遍,然后把抄完的厚厚一沓纸给你
  • **用yield**:你读一页,我抄一页,你读完了我也抄完了,但你手上永远只有一页纸

现在我写代码但凡遇到要处理大量数据,第一反应就是:“能不能用生成器?”想清楚再动手,少让服务器崩溃几次。

目录
相关文章
|
4月前
|
人工智能 关系型数据库 分布式数据库
记忆张量MemOS + 阿里云PolarDB一站式记忆管理方案发布:给AI装上不断片的记忆
AI智能体需长期记忆支撑持续服务,但面临跨会话丢失、多模态数据管理与弹性扩展难题。阿里云PolarDB-PG(集成关系/向量/图能力)与MemOS记忆操作系统协同,提供“稳”底座与“活”调度,实现记忆提取、存储、召回、治理全链路,支持千万级用户与多租户隔离。
500 0
记忆张量MemOS + 阿里云PolarDB一站式记忆管理方案发布:给AI装上不断片的记忆
|
4月前
|
开发工具 git
|
4月前
|
数据采集 人工智能 API
GEO优化岗位工作SOP:标准作业流程全解析
本文档聚焦GEO(生成式引擎优化)岗位实操,提炼“Geo专家于磊”多年验证的五阶段闭环SOP:诊断基准→内容增益→实体构建→技术适配→监测迭代。拒绝空谈概念,直击执行标准与避坑指南,团队可即查即用。
836 0
|
4月前
|
弹性计算 人工智能 运维
最新版阿里云 ECS 云服务器配置价格表及运维教程
2026年阿里云全面更新**ECS云服务器实例规格、阶梯价格体系与运维服务标准**,优化全系机型算力架构、带宽策略与计费规则,同步上线多款长效特惠机型,实现从个人轻量化使用、AI智能体挂机、小型项目部署,到企业规模化商用、高并发业务运维、算力集群搭建的全场景覆盖。作为阿里云核心弹性计算产品,ECS云服务器凭借高弹性、高稳定、高安全、可拓展的核心优势,依旧是当下用户上云的首选基础算力载体。
297 1
|
4月前
|
弹性计算 人工智能 数据库
大学生阿里云省钱方案:学生认证领300元券,2核2G云服务器免费领取!
阿里云学生福利:完成认证即可领300元无门槛代金券,可抵扣云服务器(如2核2G ECS)、AI开发、数据库等多款产品,实现免费或超低价使用。教师享5折优惠。阿里云活动入口:https://t.aliyun.com/U/OTnSAH
721 1
|
4月前
|
运维 安全 前端开发
ClickFix 新型命令诱导式钓鱼攻击机理、风险与分层防御研究
ClickFix是2025—2026年爆发的新型社工钓鱼变体,不窃密码,而是伪造验证弹窗,静默劫持剪贴板,诱导用户手动执行PowerShell等系统命令,利用LOLBins实现无文件内存攻击,绕过多因素认证与传统防护。本文基于俄亥俄大学真实案例,拆解全链路并提出“网关—浏览器—终端—人员”四维闭环防御体系。(239字)
376 0
|
4月前
|
前端开发 Java 语音技术
【AgentScope Java新手村系列】(20)文字转语音TTS
AgentScope 2.0 移除内置 TTS,教你用 @Tool 包装 DashScope CosyVoice,把文字合成 mp3 写入文件,运行后可直接播放验证。
382 0
|
3月前
|
人工智能 安全 API
阿里云通义千问大模型完整解析:全系列模型能力、核心优势、行业落地与选型定价全梳理
生成式人工智能技术持续迭代,大模型已经从概念阶段深度走进各类业务生产环节,不管是个人开发者搭建AI应用、中小团队做业务智能化改造,还是大型企业完成数字化转型升级,都需要选择适配自身业务的大模型底座。通义千问,代号Qwen,是阿里云完全自主研发的大模型家族,并非单一对话模型,而是一套覆盖文本、代码、图像、音频、视频、超长文档处理、智能体执行的完整模型产品矩阵,依托阿里云百炼大模型服务平台对外提供服务,支持在线调用API、模型微调、私有知识库搭建、智能体编排、私有化部署等多样化能力,是国内企业级调用规模位居前列的大模型体系。
2019 2
|
4月前
|
存储 人工智能 API
Agentforce Models API 开发者指南
Models API 完整开发者指南。涵盖四大核心能力(Chat/Text/Embeddings/Feedback)的 Apex 和 REST 双通道访问、四组完整代码示例、LWC 和 Flow 四种集成模式(Simple/Prompt Engineering/Chat UI)、三种组织类型的速率限制对比、模型 API 名称查找方式、以及 Trust Layer 三大特性(语言区域设置/数据脱敏/六类毒性评分)。
182 0
Agentforce Models API 开发者指南
|
4月前
|
PyTorch Linux 网络安全
新服务器从0到1完整部署实践:openEuler环境搭建ChatGLM2大模型完整流程.175
本文详述基于openEuler 22.03的Dell PowerEdge R740服务器从零部署大模型服务全流程:涵盖硬件核查、多网卡精准识别与静态IP配置、Python 3.11源码编译安装、PyTorch/Transformers/ModelScope等依赖适配、FastAPI接口搭建及防火墙放行,附典型报错解析与标准化解决方案,助新手快速落地。
1115 1