迭代器与生成器:数据大到装不下时
约 14 分钟
前面处理的数据都能整个装进内存。但真实的数据集经常几个 GB 甚至更大——这一节讲怎么办。
问题所在
data = [process(line) for line in open("huge.txt")] # ⚠️ 全部装进内存
一个 10GB 的文件,这行代码会直接把内存吃光。
核心思路:不要一次性全拿,用一个算一个。
生成器:把 return 换成 yield
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.strip() # 不是 return
for line in read_lines("huge.txt"):
process(line) # 内存里始终只有一行
yield 和 return 的区别:
| return | yield | |
|---|---|---|
| 执行到它 | 函数结束 | 暂停,交出一个值 |
| 下次调用 | 从头开始 | 从暂停处继续 |
| 返回 | 一个值 | 一个生成器对象 |
关键理解:生成器函数被调用时,函数体一行都不会执行——它只是返回一个生成器对象。只有当你开始迭代(for 循环或 next()),代码才真正跑起来,而且每次只跑到下一个 yield。
生成器表达式:把方括号换成圆括号
squares_list = [x**2 for x in range(10_000_000)] # 列表推导式:立刻算完,占内存
squares_gen = (x**2 for x in range(10_000_000)) # 生成器表达式:用到才算
print(sum(squares_gen)) # 边算边加,内存占用几乎为零
一个字符的差别,内存占用差了几个数量级。
惰性计算的两个好处
① 省内存(上面已经说了)
② 能表示无限序列
def count_from(n):
while True:
yield n
n += 1
for i in count_from(1):
if i > 5:
break
print(i) # 1 2 3 4 5
列表做不到这一点——无限长的列表根本构造不出来。
itertools:处理迭代器的标准工具箱
import itertools
itertools.islice(gen, 10) # 只取前 10 个
itertools.chain(gen1, gen2) # 把多个迭代器串起来
itertools.groupby(sorted_data, key=fn) # 分组(要求已排序)
itertools.product([1,2], ["a","b"]) # 笛卡尔积
islice 在调试时特别有用——处理大文件前,先只取前 10 条看看逻辑对不对,不用等全部跑完。
pandas 也支持分块读
for chunk in pd.read_csv("huge.csv", chunksize=100_000):
result = process(chunk) # 每次处理 10 万行
这是处理超大 CSV 的标准做法。
生成器的一个限制:只能遍历一次
gen = (x for x in range(5))
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] —— 已经耗尽了!
这是最容易踩的坑。 如果需要多次遍历,要么转成列表(list(gen)),要么重新创建生成器。
为什么这一节对学 AI 重要
深度学习的数据加载完全建立在这个思想上。 PyTorch 的 DataLoader 本质就是一个迭代器:
for batch in dataloader: # 每次只把一批数据读进内存和显存
train_step(batch)
理解了生成器,你就理解了为什么能用 8GB 显存训练远大于 8GB 的数据集。
练习:写一个生成器函数,逐行读取一个文本文件并只 yield 出包含指定关键词的行,然后用 itertools.islice 只看前 5 条结果。
小纸条
yield 和 return 的区别是什么?生成器最容易踩的坑是什么?这和深度学习的 DataLoader 有什么关系?
登录 后可看答案