首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的生成器把我坑惨了,原来yield和return的区别这么大

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

原创
作者头像
风一样的男子
发布2026-07-20 10:50:48
发布2026-07-20 10:50:48
420
举报
文章被收录于专栏:编程教程编程教程

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

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

我当时的方案很简单:

代码语言:javascript
复制
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的区别彻底研究了一遍。今天把这些东西讲清楚,希望你下次遇到类似问题别像我一样慌。

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

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

代码语言:javascript
复制
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就不一样了。

代码语言:javascript
复制
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改造一下:

代码语言:javascript
复制
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暂停函数

看这段代码:

代码语言:javascript
复制
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()的时候,它才会开始跑:

代码语言:javascript
复制
gen = demo_yield()
print(next(gen))  # 打印"第1步",返回"结果1",暂停
print(next(gen))  # 打印"第2步",返回"结果2",暂停
print(next(gen))  # 打印"第3步",然后函数结束,抛出StopIteration

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

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

能。但有坑。

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

代码语言:javascript
复制
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字节——差距是几万倍

第六步:还有几个坑

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

代码语言:javascript
复制
gen = (x for x in range(5))
print(list(gen))  # [0, 1, 2, 3, 4]
print(list(gen))  # [] —— 空了!

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

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

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

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

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

代码语言:javascript
复制
# 列表推导式——立即生成所有数据
squares_list = [x**2 for x in range(10)]  # 方括号

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

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

总结

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

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

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

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个让服务器差点崩了的Bug
  • 第一步:return——一次性交作业
  • 第二步:yield——边做边上菜
  • 第三步:核心区别——暂停 vs 结束
  • 第四步:yield和return能不能一起用?
  • 第五步:什么时候该用yield?
  • 第六步:还有几个坑
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档