Python的秘密生活:懒惰的面包师

发布: (2026年1月18日 GMT+8 12:14)
7 min read
原文: Dev.to

Source: Dev.to

请提供您希望翻译的具体文本内容,我将为您翻译成简体中文并保持原有的格式、Markdown 语法以及技术术语不变。

Source:

为什么你的 Python 代码会耗尽内存,以及 yield 如何解决它

图书馆里弥漫着淡淡的臭氧和燃烧塑料的味道。Timothy 坐在笔记本电脑前,但他并没有敲键盘。他呆呆地盯着旋转的光标。风扇在尖叫。

“它又崩溃了,” Timothy 抱怨道。

Margaret 在书中划下书签,抬头看了过来。

“你想做什么?”

“我需要模拟一个数据集,” Timothy 解释说。“我需要一亿条随机传感器读数来测试我的新分析脚本。我写了一个函数把它们生成并放进列表里。”

他指着屏幕上冻结的代码。

Timothy 的 “急切” 面包师

def produce_readings(n):
    results = []
    print("Starting production...")
    for i in range(n):
        # Imagine complex calculation here
        results.append(i * 1.5)
    print("Production complete!")
    return results

# Asking for 100 million items
data = produce_readings(100_000_000)

for reading in data:
    process(reading)

“我点了运行,” Timothy 说,“然后电脑就卡住了。根本进不到 process 循环。它直接耗尽了内存。”

Margaret 知道地点了点头。

“你雇了 急切的面包师。”

面包店类比

“想象一家面包店。你走进去要了一万条面包。
急切的面包师 说,‘好的,先生!’ 他立刻把一万团面团揉好,烤出一万条面包,堆满柜台、地板、后仓……面包店要爆炸了。他在所有一万条面包都烤好之前,根本不能给你一条。”

“这正是你的列表在做的事,” Margaret 说。“你请求了一亿个数字。Python 正在把它们全部生成、全部存入 RAM,然后才让你使用第一个。你的电脑根本没有足够大的仓库。”

“那我就不能处理大数据了吗?” Timothy 问。

“可以的,” Margaret 笑道,“但你必须解雇 急切的面包师。你需要 懒惰的面包师。”

Margaret 的 “懒惰” 面包师(生成器)

def produce_readings_lazy(n):
    print("Starting production...")
    for i in range(n):
        yield i * 1.5
    print("Production complete!")

“列表呢?” Timothy 问。“results.append 呢?”

“根本没有列表,” Margaret 说。“只有 yield。这个关键字把函数变成了 生成器。”

使用生成器

# Create the generator object
baker = produce_readings_lazy(100_000_000)

print(f"Baker object: {baker}")

# Ask for the first loaf
print(next(baker))

# Ask for the second loaf
print(next(baker))

输出

Baker object: <generator object produce_readings_lazy at 0x...>
Starting production...
0.0
1.5

“看到了吗?” Margaret 低声说。“当我们创建 baker 时,什么都没发生。没有代码执行,也没有占用内存。面包师在等候。”

“当我们调用 next(baker) 时,它被唤醒。它运行到 yield,把一个值交给你,然后暂停,记住自己停下的地方。”

“所以它一次只烤一条?” Timothy 问。

“按需烤,” Margaret 确认道。“它烤一条,交给你,然后等你再来。它永远不会一次存储超过一个项目。”

内存对比

Margaret 导入 sys 来衡量对象大小。

import sys

# The Eager List (small scale to avoid crashing)
eager_list = [i * 1.5 for i in range(1_000_000)]

# The Lazy Generator (same scale)
lazy_gen = (i * 1.5 for i in range(1_000_000))

print(f"List Size in Bytes:      {sys.getsizeof(eager_list)}")
print(f"Generator Size in Bytes: {sys.getsizeof(lazy_gen)}")

结果

List Size in Bytes:      8448728
Generator Size in Bytes: 104

“列表大约 8 MB。生成器只有… 104 字节?比一条推文还小。”

“因为列表保存了一百万个数字。生成器只保存获取下一个数字的配方。无论你请求多少,都不会占用额外的内存。”

“他甚至可以一直烘烤下去,”玛格丽特补充道。
“如果你写 while True: yield n,他永远不会用完面团。只要你一次只取一个,他就会无限烘烤。”

The Catch

“有一点要注意,”玛格丽特警告道。
“懒惰面包师没有存放空间。一旦他把面包递给你,他就会把它忘掉。除非重新启动整个过程,否则你不能再请求‘第 5 个面包’。生成器是一条单向街道。”

“但是,”她快速敲键盘时说,“你可以把它们串联成管道。”

# The Pipeline: Data flows through, one item at a time
raw_data   = (read_sensor() for _ in range(1_000_000))
clean_data = (x for x in raw_data if x > 0)
final_data = (process(x) for x in clean_data)

“没有列表,”蒂莫西惊叹道。
“只有流。”

Quick Reference (Margaret’s Notebook)

  • The Eager Approach (Lists) – 一次性计算所有内容。随机访问快(my_list[5]),但占用大量内存。
  • The Lazy Approach (Generators) – 按需一次计算一个元素。
  • yield Keyword – 暂停函数,返回一个值,并保存其状态。
  • Generator Expressions(x for x in data)。类似列表推导式,但使用 ()
  • Trade‑off – 生成器可以节省巨量内存,但只能使用一次。不能对其进行索引(gen[0])或二次迭代。
  • Use Cases – 大规模数据集、无限流、数据管道。

蒂莫西把方括号换成了圆括号,点击 Run。笔记本的风扇立刻安静下来,数据开始流动。

“我从没想过会说这句话,”蒂莫西笑着说,“但我爱上了懒惰。”

“效率,蒂莫西,”玛格丽特端着茶纠正道,“我们叫它效率。”

在下一集,玛格丽特和蒂莫西将面对 “魔法书架”——他们将在那里发现一种能够…的数据结构。

Aaron Rose 是 tech-reader.blog 的软件工程师兼科技作家,也是《Think Like a Genius》的作者。

Back to Blog

相关文章

阅读更多 »

SQLite 中缓存的效率

SQLite 中 Cache 的效率!封面图片用于 “SQLite 中 Cache 的效率” https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=aut...

Python:Tprof,目标分析器

Python:介绍 tprof,一款 targeting profiler。Profilers 测量整个程序的性能,以识别大部分时间消耗的位置。但一旦你…