Python 迭代器与生成器精讲:大幅降低内存占用
处理大规模数据时,内存往往是第一道瓶颈。列表一次要把所有数据加载进内存,100 万条记录可能直接吃掉几百 MB。迭代器和生成器,就是 Python 给你的省内存利器。
一、先搞清:什么是可迭代对象
能被 for 循环遍历的,都叫可迭代对象(Iterable) 。
python
python
list, tuple, str, dict, set → 都是可迭代对象
但可迭代 ≠ 迭代器。
二、迭代器(Iterator):边用边取,用完即丢
迭代器是一个记住了当前位置的对象,每次只返回一个值,取完就没了。
python
python
it = iter([1, 2, 3])
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
print(next(it)) # StopIteration ← 取完了
关键特性:
| 特性 | 说明 |
|---|---|
有 __iter__() 方法 |
返回自己 |
有 __next__() 方法 |
返回下一个值,没有则抛 StopIteration |
| 只能前进,不能回退 | 像磁带,不能倒带 |
| 只能遍历一次 | 读完就空了 |
列表是可迭代对象,但不是迭代器:
python
python
lst = [1, 2, 3]
hasattr(lst, '__next__') # False
it = iter(lst)
hasattr(it, '__next__') # True ← 这才是迭代器
for 循环的本质,就是不断调用 next(),直到捕获 StopIteration。
三、生成器(Generator):写起来像函数,用起来像迭代器
生成器是一种特殊的迭代器 ,不用写类,用 yield 就能造出来。
1. 生成器函数
python
python
def count_up_to(n):
i = 1
while i <= n:
yield i # 暂停,返回 i,下次从这里继续
i += 1
gen = count_up_to(5)
print(next(gen)) # 1
print(next(gen)) # 2
yield 和 return 的区别:
return |
yield |
|
|---|---|---|
| 执行后 | 函数结束,局部变量销毁 | 暂停,局部变量保留 |
| 调用次数 | 一次 | 可以多次 resume |
| 返回值 | 一个结果 | 每次返回一个值 |
2. 生成器表达式
语法和列表推导式一样,只是把 [] 换成 ():
ini
python
# 列表推导式 → 一次性生成所有数据,占内存
squares_list = [x**2 for x in range(1000000)] # ~40 MB
# 生成器表达式 → 用一个取一个,几乎不占内存
squares_gen = (x**2 for x in range(1000000)) # ~几十字节
四、内存对比:数据说话
处理 1000 万个整数的平方:
| 方式 | 内存占用 | 说明 |
|---|---|---|
列表 [x**2 for x in range(10_000_000)] |
~80 MB | 全部加载进内存 |
生成器 (x**2 for x in range(10_000_000)) |
~120 字节 | 只存当前状态 |
迭代器 iter(range(10_000_000)) |
~48 字节 | 连计算都省了 |
差距是 6 位数级别的。
五、实战:什么时候该用生成器
✅ 适合用生成器的场景
| 场景 | 原因 |
|---|---|
| 读取大文件(GB 级) | 不用一次性读入内存 |
| 数据管道/流处理 | 上游产一个,下游消一个 |
| 无限序列 | 列表存不下,生成器可以无限产生 |
| 中间结果不需要保留 | 用完就丢,没必要存 |
示例:逐行读取大文件
python
python
# ❌ 错误:一次性读入内存
with open('huge_log.txt') as f:
lines = f.readlines() # 内存爆炸
# ✅ 正确:生成器逐行读取
with open('huge_log.txt') as f:
for line in f: # f 本身就是迭代器
if 'ERROR' in line:
print(line)
open() 返回的文件对象本身就是迭代器,每次 for 循环只读一行。
示例:数据管道
python
python
def read_log(filename):
with open(filename) as f:
for line in f:
yield line.strip()
def filter_errors(lines):
for line in lines:
if 'ERROR' in line:
yield line
def extract_time(lines):
for line in lines:
yield line.split()[0]
# 管道组合:数据从左到右流动,全程不存中间结果
pipeline = extract_time(filter_errors(read_log('app.log')))
for t in pipeline:
print(t)
三个生成器串联,内存占用始终只有一行数据的大小。
六、yield from:生成器嵌套的简洁写法
csharp
python
def gen1():
yield 1
yield 2
def gen2():
yield from gen1() # 等价于逐个 yield gen1() 的值
yield 3
print(list(gen2())) # [1, 2, 3]
yield from 还能透明传递 send() 和 throw(),是生成器组合的推荐写法。
七、常见误区
| 误区 | 真相 |
|---|---|
| 生成器比列表快 | 不一定。生成器省内存,但有 yield 开销。小数据用列表更快 |
| 生成器可以倒回去 | 不能。要重遍历,重新调用函数 |
yield 后的代码不执行 |
执行,只是暂停。下次 next() 从 yield 下一行继续 |
| 生成器表达式可以重复用 | 不能。用完就空了,要重新创建 |
八、一张表总结
| 概念 | 本质 | 内存 | 复用性 | 典型写法 |
|---|---|---|---|---|
| 列表 | 一次性存储所有元素 | 高 | ✅ 可重复 | [x for x in range(n)] |
| 迭代器 | 边用边取,记住位置 | 极低 | ❌ 一次性 | iter(list) |
| 生成器 | 用 yield 实现的迭代器 |
极低 | ❌ 一次性 | (x for x in range(n)) |
| 生成器函数 | 包含 yield 的函数 |
极低 | ❌ 一次性 | def f(): yield x |
核心结论
- 内存紧张时,能用生成器就别用列表。
for循环天然支持迭代器,改起来成本很低。- 文件对象、
range()、map()、filter()本身都是迭代器,直接用就行,别包一层list()。
省下来的内存,就是你程序能处理的数据量上限。