Python 编程

迭代器与生成器:数据大到装不下时

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)                    # 内存里始终只有一行

yieldreturn 的区别

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 有什么关系?

登录 后可看答案