Python的秘密生活:懒惰的面包师
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) – 按需一次计算一个元素。
yieldKeyword – 暂停函数,返回一个值,并保存其状态。- Generator Expressions –
(x for x in data)。类似列表推导式,但使用()。 - Trade‑off – 生成器可以节省巨量内存,但只能使用一次。不能对其进行索引(
gen[0])或二次迭代。 - Use Cases – 大规模数据集、无限流、数据管道。
蒂莫西把方括号换成了圆括号,点击 Run。笔记本的风扇立刻安静下来,数据开始流动。
“我从没想过会说这句话,”蒂莫西笑着说,“但我爱上了懒惰。”
“效率,蒂莫西,”玛格丽特端着茶纠正道,“我们叫它效率。”
在下一集,玛格丽特和蒂莫西将面对 “魔法书架”——他们将在那里发现一种能够…的数据结构。
Aaron Rose 是 tech-reader.blog 的软件工程师兼科技作家,也是《Think Like a Genius》的作者。