# Python 生成器为什么会被“消耗掉”?——一次性与可重复迭代的区别
·
在写深度学习项目时,我遇到一个让人摸不着头脑的 bug:
训练数据迭代器第一次还能打印出正常的 batch,但一旦传给训练函数,就直接报错:
ZeroDivisionError: float division by zero
追踪代码后发现,问题根源在于 **Python 的生成器 (generator) 是一次性的**。这篇文章就来彻底讲清楚这个问题。
---
## 1. 列表 vs 生成器
在 Python 里,**列表**和**生成器**有本质上的区别:
- **列表**:数据存放在内存里,可以随便访问任意下标,取多少次都行。
- **生成器**:数据是“现用现造”的,每次迭代会把指针往前推进,走过的就没了,不能回头。
简单对比一下:
```python
# 列表:多次迭代没问题
lst = [0, 1, 2]
for x in lst:
print("first loop:", x)
for x in lst:
print("second loop:", x)
# 生成器:只能用一次
def gen():
for i in range(3):
yield i
g = gen()
for x in g:
print("first loop:", x)
for x in g:
print("second loop:", x)
出结果:
first loop: 0
first loop: 1
first loop: 2
second loop: 0
second loop: 1
second loop: 2
---
first loop: 0
first loop: 1
first loop: 2
# 第二次循环什么都没有!
为什么?因为生成器在第一次迭代时已经跑到结尾,第二次再迭代时里面已经空了。
2. 生成器的本质是状态机
理解这个现象的关键:生成器是状态机。
比如:
def gen():
for i in range(3):
print("生成:", i)
yield i
g = gen()
print(next(g)) # 第一次 next
print(next(g)) # 第二次 next
输出:
生成: 0
0
生成: 1
1
可以看到,生成器不是“一次性算完放内存”,而是运行到 yield 就停下,下一次再从上次停的地方继续跑。等所有 yield 执行完,它就抛出 StopIteration,彻底结束。
所以说,生成器就像 传送带,东西经过一次就没了;而列表像 冰箱,随时都能取。
📌 对比图示:
3. 在训练代码中的坑
在深度学习代码里,我定义了一个数据加载函数:
def load_data_time_machine(batch_size, num_steps):
corpus = torch.arange(100) # 假装语料
def seq_data_iter_random(corpus, batch_size, num_steps):
for i in range(0, len(corpus) - num_steps, num_steps):
X = corpus[i:i+num_steps]
Y = corpus[i+1:i+1+num_steps]
yield X, Y
return seq_data_iter_random(corpus, batch_size, num_steps)
然后这样用:
train_iter = load_data_time_machine(32, 35)
# 第一次迭代,正常
for X, Y in train_iter:
print(X.shape, Y.shape)
break
# 第二次迭代,啥也没了
for X, Y in train_iter:
print("again:", X.shape, Y.shape)
结果就是:第一次能打印,第二次完全没有输出。因为 train_iter 是生成器,只能用一次。
当我把这个“已经消耗掉的迭代器”传进训练函数时,里面啥数据都没了,于是 metric[1] == 0,报出除零错误。
4. 解决方案
如果你希望数据加载器能多次使用,有两种方法:
方法 1:每次训练前重新生成迭代器
train_iter = load_data_time_machine(batch_size, num_steps)
train_ch8(net, train_iter, vocab, lr, num_epochs, device)
这样保证每次都是新的生成器。
方法 2:写一个可重复迭代的类
class SeqDataLoader:
def __init__(self, corpus, batch_size, num_steps):
self.corpus = corpus
self.batch_size = batch_size
self.num_steps = num_steps
def __iter__(self):
for i in range(0, len(self.corpus) - self.num_steps, self.num_steps):
X = self.corpus[i:i+self.num_steps]
Y = self.corpus[i+1:i+1+self.num_steps]
yield X, Y
这样返回的 train_iter 就能多次使用,不会“用一次就没了”。
5. 总结
-
生成器不是列表,它不会把数据存下来,只是动态生成。
-
一旦你迭代完,生成器就到头了,不能再用了。
-
在深度学习训练里,这会导致数据加载器第二次迭代时为空,出现各种奇怪的 bug(比如 ZeroDivisionError)。
-
解决办法:要么每次重新生成迭代器,要么改写成可重复迭代的类。
记住一句话:
列表是“冰箱”,随时可取;生成器是“水龙头”,水流走了就没了。
更多推荐
所有评论(0)