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服务器根本扛不住。

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

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

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

代理 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 结束

returnyield最本质的区别就一句话:

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**:你读一页,我抄一页,你读完了我也抄完了,但你手上永远只有一页纸

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

目录
相关文章
|
25天前
|
数据采集 人工智能 API
GEO优化岗位工作SOP:标准作业流程全解析
本文档聚焦GEO(生成式引擎优化)岗位实操,提炼“Geo专家于磊”多年验证的五阶段闭环SOP:诊断基准→内容增益→实体构建→技术适配→监测迭代。拒绝空谈概念,直击执行标准与避坑指南,团队可即查即用。
281 0
|
21天前
|
数据采集 人工智能 自然语言处理
自动化比价系统:从采集到数据清洗,全链路打通教程
本文详解淘宝/京东/拼多多三平台自动化比价系统全链路:用OpenClaw自然语言采集、站大爷隧道代理防封(24小时成功率98.2%+)、AI智能清洗价格(统一格式、核销优惠、去重校验),自动生成可决策的比价报告与飞书预警,真正实现“采得稳、洗得准、用得上”。
154 1
|
22天前
|
数据采集 调度 Python
Python的异步把我坑惨了,原来async/await和多线程的区别这么大
这是一个真实爬虫项目复盘:同步耗4小时,多线程易被封IP,最终用asyncio+aiohttp异步方案,单线程10分钟搞定万级请求,性能提升10–100倍,资源占用低、并发可控。(239字)
89 0
|
25天前
|
存储 人工智能 API
Agentforce Models API 开发者指南
Models API 完整开发者指南。涵盖四大核心能力(Chat/Text/Embeddings/Feedback)的 Apex 和 REST 双通道访问、四组完整代码示例、LWC 和 Flow 四种集成模式(Simple/Prompt Engineering/Chat UI)、三种组织类型的速率限制对比、模型 API 名称查找方式、以及 Trust Layer 三大特性(语言区域设置/数据脱敏/六类毒性评分)。
Agentforce Models API 开发者指南
|
25天前
|
人工智能 关系型数据库 分布式数据库
记忆张量MemOS + 阿里云PolarDB一站式记忆管理方案发布:给AI装上不断片的记忆
AI智能体需长期记忆支撑持续服务,但面临跨会话丢失、多模态数据管理与弹性扩展难题。阿里云PolarDB-PG(集成关系/向量/图能力)与MemOS记忆操作系统协同,提供“稳”底座与“活”调度,实现记忆提取、存储、召回、治理全链路,支持千万级用户与多租户隔离。
134 0
记忆张量MemOS + 阿里云PolarDB一站式记忆管理方案发布:给AI装上不断片的记忆
|
25天前
|
弹性计算 人工智能 数据库
大学生阿里云省钱方案:学生认证领300元券,2核2G云服务器免费领取!
阿里云学生福利:完成认证即可领300元无门槛代金券,可抵扣云服务器(如2核2G ECS)、AI开发、数据库等多款产品,实现免费或超低价使用。教师享5折优惠。阿里云活动入口:https://t.aliyun.com/U/OTnSAH
127 1
|
25天前
|
弹性计算 人工智能 运维
最新版阿里云 ECS 云服务器配置价格表及运维教程
2026年阿里云全面更新**ECS云服务器实例规格、阶梯价格体系与运维服务标准**,优化全系机型算力架构、带宽策略与计费规则,同步上线多款长效特惠机型,实现从个人轻量化使用、AI智能体挂机、小型项目部署,到企业规模化商用、高并发业务运维、算力集群搭建的全场景覆盖。作为阿里云核心弹性计算产品,ECS云服务器凭借高弹性、高稳定、高安全、可拓展的核心优势,依旧是当下用户上云的首选基础算力载体。
89 1
|
21天前
|
数据采集 机器学习/深度学习 Java
Python 的多线程就是个摆设?不,原来是你没选对IO场景,这差距大到离谱
本文深入剖析Python多线程性能之谜:揭秘GIL机制如何限制CPU密集型任务的并行效率,同时阐明其在IO密集型场景(如爬虫、文件读写)中的显著优势。通过实测对比,清晰指出——CPU型任务选多进程,IO型任务用多线程,并附实战选型指南与Python 3.13自由线程前瞻。
103 0
|
23天前
|
人工智能 文字识别 API
AI大模型赋能企业跨端远程办公与文件处理:用版本比对自动生成变更摘要的工程方法
企业远程协作中,真正耗时的往往不是打开文件,而是确认“这次到底改了什么”。本文介绍如何将传统版本比对与大模型摘要结合,把 Word、PDF、制度文档、项目方案的变更转换为可复核的变更清单、审批任务和跨端通知。
142 0
|
25天前
|
开发工具 git