在写深度学习项目时,我遇到一个让人摸不着头脑的 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)。

  • 解决办法:要么每次重新生成迭代器,要么改写成可重复迭代的类。

记住一句话:

列表是“冰箱”,随时可取;生成器是“水龙头”,水流走了就没了。

更多推荐