page contents

什么是 Python 中的生成器(generator)?你为什么会使用它?

那天吃完午饭,我正靠在工位上打着瞌睡,突然我们组的实习生跑过来问我一句:“哥,Python 的 generator 到底是干嘛的?我看文档上写能节省内存,但我一点没体会到啊。”我一听这个问题,瞬间清醒了。因为这事儿我当年也懵过,直到有一次线上日志炸了,才算真理解了生成器的妙处。

attachments-2025-10-mUsqGEFA68f5d2282348f.png那天吃完午饭,我正靠在工位上打着瞌睡,突然我们组的实习生跑过来问我一句:“哥,Python 的 generator 到底是干嘛的?我看文档上写能节省内存,但我一点没体会到啊。”我一听这个问题,瞬间清醒了。因为这事儿我当年也懵过,直到有一次线上日志炸了,才算真理解了生成器的妙处。

先这么说吧,generator 就是 Python 里一个能“边干活边产出结果”的家伙。你平时写的函数,一般都是“全做完再一次性返回结果”,比如:

def get_numbers():
    return [i for i in range(1000000)]

这种函数看起来挺正常的,但如果你打印下 sys.getsizeof(get_numbers()),那内存直接飙上去。因为它会在内存里一下子创建出一百万个数字。那要是再大点,比如十亿个?电脑直接卡死。

而生成器不一样。它不是一次性造出所有结果,而是“你要一个我给一个”,就像外卖骑手:你点一份我送一份,不会一次性全堆门口。你写成这样:

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

关键字 yield 就是生成器的灵魂,它会让函数变成一种“可迭代对象”,但不会立刻执行完。当你去遍历它的时候,它才慢慢“吐出”结果。内存瞬间轻松了,系统也不会崩。

我还记得当时是怎么真切体会到 generator 的威力的。那天线上跑一个日志分析脚本,里面要读取几十个 GB 的日志文件。我写的那版是直接 readlines() 一次读进内存,结果整个服务器直接 OOM。我被领导点名复盘的时候脸都绿了。后来同事教我用生成器改写:

def read_logs(filename):
    with open(filename, 'r'as f:
        for line in f:
            yield line.strip()

这段代码看起来没啥特别的,但它神奇的地方就在这里——文件是逐行读的,不会一次性塞到内存。整个脚本从十几分钟卡死,变成稳定跑完几小时,内存占用才几十 MB。那次之后我彻底信了 generator 的魔法。

那你可能会问了,除了省内存,还有啥好处?其实还有两个很实用的点:一个是提高性能,另一个是简化异步逻辑。

先说性能。举个例子,比如你要找出一个列表中所有偶数的平方。如果用列表推导式:

squares = [x*x for x in numbers if x % 2 == 0]

这没问题,但会立刻创建一个完整的列表。而生成器表达式就像是它的“懒加载版本”:

squares = (x*x for x in numbers if x % 2 == 0)

区别就在一个小括号。这个版本不会立刻生成全部数据,只有当你遍历它时才会逐步算出来。对于大型数据处理,这个差距是质变的。

我那会儿写日志聚合分析时,就是用这种方式。以前整份日志文件全进内存,现在一行一行过滤、处理、统计,用生成器串起来,代码可读性高、性能还稳。

再说异步。其实 Python 的协程(async/await)机制就是从生成器演化来的。早期版本还没有协程语法时,就是用 yield 来实现“暂停和恢复”的。比如:

def countdown(n):
    while n > 0:
        yield n
        n -= 1

这个 yield 本质上可以理解为一个“暂停点”。执行到那一步时,它会保存当前函数状态,下次再调用时,从这个状态继续往下走。这不就是协程干的事嘛?所以后来才有了更高级的 async def 写法,但本质思路一脉相承。很多框架(比如 Tornado、早期的 asyncio)底层都靠生成器实现异步调度。

不过说句实话,generator 也不是万能的。我刚接触的时候,特别容易写出一堆乱七八糟的 yield 函数,自己都不知道状态在哪。比如这个:

def weird():
    print("Start")
    yield 1
    print("Middle")
    yield 2
    print("End")

当你用 next() 调用它的时候,它不是一次性执行,而是每次执行到 yield 停下来。比如:

g = weird()
next(g)
next(g)
next(g)

输出依次是:

Start
Middle
End

它其实就像是函数的“断点续传”。每次 yield 都会暂停执行,等下次再恢复。如果你脑子不太清楚逻辑,调试起来就跟捉迷藏一样。尤其是在多层嵌套生成器的时候,真的容易把自己绕晕。

我还记得有次写爬虫的时候,用生成器嵌套生成器,一个负责抓页面,一个负责解析链接,还有一个控制调度。结果我当时加了个错误重试的逻辑,yield 嵌进 try-except 里,异常一出来整个生成链断了。那会儿我差点怀疑人生。后来才学会用 yield from 把子生成器“代理”出来,这样结构清晰多了:

def fetch_all(urls):
    for url in urls:
        yield from fetch_page(url)

这句 yield from 等价于把 fetch_page 里的所有 yield 都打平出来,既省代码又逻辑顺滑。也是 Python 3 之后才支持的一个小亮点。

讲到这其实差不多该总结一下了。generator 这个东西,简单说就是:它能让你在需要时才产出数据,不浪费内存,还能天然支持异步逻辑。它的本质是“惰性求值”——你不去取,它就不计算。听着简单,但真正灵活运用起来,能让代码又快又优雅。

尤其在处理流式数据(文件、日志、网络请求)时,生成器几乎是唯一正确的选择。以前你可能要写一堆缓存、分页逻辑,现在一句 yield 就搞定。节约资源的同时,还让代码更贴近“数据流”的思维。

后来那个实习生也终于明白了,过来和我说:“原来 generator 就是个‘边走边算’的函数啊。”我笑着说,对,它像个“节奏掌控者”,不是一口气冲刺,而是每一步都精准输出。其实编程很多时候就是这样——不是要让电脑做得更多,而是让它做得刚刚好

所以啊,Python 的生成器看似平淡,背后藏着效率的哲学。你要真懂了它,就能写出更轻盈、更可控的程序。再也不会因为一个几百 MB 的文件把内存挤爆,也不会被“等所有结果算完再返回”的老逻辑拖垮。那天我跟实习生说完这话,转头自己想想,也挺感慨的——学技术这事,很多时候就是得自己卡过一两次,才会真心服。

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

  • 发表于 2025-10-20 14:09
  • 阅读 ( 27 )
  • 分类:Python开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1479 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 2228 文章
  3. Pack 1479 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章