上一章讲清了 for 背后的原理,这一章开始自己动手造一个迭代器

上一章已经知道,for 循环不是在“认识列表”,而是在驱动一套统一规则:

先拿到迭代器
再不断取下一个值
取不到时,用 StopIteration 结束

知道原理,和真正会写,还差一步。
这一步就是:自己实现一个迭代器。

很多人对迭代器一直停留在会用,不会做。
一看到 __iter____next__ 就紧张,感觉像底层黑魔法。其实真拆开看,逻辑并不复杂。

你只要弄清三个问题:

谁负责保存当前位置
谁负责产出下一个值
什么时候该结束

这一章就围绕这三件事,把迭代器从零写透。看懂后,你不仅知道它怎么工作,还会知道为什么有的对象能重复遍历,有的对象一遍走完就空了。

先记住一句最实用的话:迭代器就是一个会记住进度的取值器

先别急着背定义。
你可以把迭代器想成一个会记住当前位置的小机器。

它每次被问一句:下一个是谁。
它就返回一个值。
同时悄悄把自己的位置往后挪一格。
如果后面已经没有值了,它不会乱返回,而是明确抛出 StopIteration。

所以迭代器和普通容器最大的区别是:

容器负责存数据
迭代器负责按顺序吐数据,并且记住自己吐到哪了

这句话要真正吃透。
因为后面所有实现,都是围绕这个核心展开的。

实现迭代器,最关键的两个方法就是 __iter____next__

一个标准迭代器,至少要有这两个方法。

__iter__()
返回迭代器对象本身

__next__()
返回下一个值,没有值时抛出 StopIteration

看一个最简单的骨架:

class MyIterator:
    def __iter__(self):
        return self

    def __next__(self):
        raise StopIteration

这段代码虽然没什么实际用处,但结构已经对了。

为什么 __iter__ 返回 self。
因为迭代器本身也是可迭代对象。
别人对它调用 iter() 时,它直接把自己交出去就行。

为什么 __next__ 要抛 StopIteration。
因为这是协议规定的结束方式。
不是返回 None,不是打印“结束了”,而是明确抛这个异常。

这一点一定要规范。
因为 for 循环和很多内置工具,都靠这个异常来判断迭代结束。

先写一个最简单的倒计时迭代器

这是最适合入门的例子之一。
因为它既简单,又能明显看到“状态在变化”。

class CountDown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration

        value = self.current
        self.current -= 1
        return value


counter = CountDown(3)

print(next(counter))
print(next(counter))
print(next(counter))

输出是:

3
2
1

再来一次:

print(next(counter))

就会报 StopIteration。

这个例子特别值得慢慢看。
因为它把迭代器最核心的动作全摆在台面上了。

self.current 负责保存当前位置
value = self.current 负责产出当前值
self.current -= 1 负责推进状态
self.current <= 0 时,说明没东西可取了,于是结束

这就是一个完整迭代器的最小模型。

为什么这里一定要先保存 value,再移动 current

很多人第一次写时容易顺手写成这样:

def __next__(self):
    self.current -= 1
    return self.current

这在某些场景下当然也能跑,但很容易把边界弄乱。
比如从 3 开始时,第一次返回的就可能变成 2。

所以写迭代器时,一个很稳的习惯是:

先判断能不能取
再保存当前值
再更新位置
最后返回当前值

也就是这种顺序:

if 结束条件:
    raise StopIteration

value = 当前值
更新状态
return value

这个顺序非常值得养成。
因为以后你写范围迭代器、文件行迭代器、分页迭代器时,基本都是这个套路。

这个迭代器也能直接放进 for,因为它满足协议

class CountDown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration

        value = self.current
        self.current -= 1
        return value


for x in CountDown(5):
    print(x)

输出就是 5、4、3、2、1。

这里要再次体会一个关键事实:

for 并不在乎这是不是列表,也不在乎它有没有索引。
它只在乎你是不是一个合格的迭代器。

只要 __iter____next__ 做对了,for 就能工作。

自己手写一个正向范围迭代器,比背定义更有用

前面是倒计时。
现在来做一个更像 range 的小工具。

class MyRange:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.end:
            raise StopIteration

        value = self.current
        self.current += 1
        return value


for x in MyRange(2, 6):
    print(x)

输出是:

2
3
4
5

这段代码已经很接近很多人脑子里对“一个一个给值”的直觉了。

只要当前值还小于 end,就继续返回。
返回后把 current 加 1。
一旦 current 到了 end,就停。

这个例子说明一件很重要的事:

迭代器不一定非得来自某种现成容器。
它完全可以是动态生成值的。

这也是迭代器强大的地方。
它不只是“遍历已有数据”,还可以“按规则逐步产出数据”。

但这个 MyRange 有一个隐藏问题:它走完一遍后,不能重新开始

看代码:

r = MyRange(1, 4)

for x in r:
    print(x)

for x in r:
    print(x)

第二个 for 什么都不会输出。
为什么。

因为这里的 MyRange 本身就是迭代器。
第一轮 for 已经把 current 走到 end 了。
第二轮再遍历时,它已经耗尽。

这正是“迭代器会记住状态”的直接体现。
它不是容器,不会自动回到初始位置。

这个现象非常重要。
因为它能帮你区分:

一个对象是“可反复产生新迭代器的可迭代对象”
还是“本身就会被消耗掉的迭代器”

如果想让对象能反复遍历,就不要让它自己直接当迭代器

这时就该把职责拆开:

可迭代对象负责保存整体信息
迭代器负责保存当前遍历进度

看改造版:

class MyRange:
    def __init__(self, start, end):
        self.start = start
        self.end = end

    def __iter__(self):
        return MyRangeIterator(self.start, self.end)


class MyRangeIterator:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.end:
            raise StopIteration

        value = self.current
        self.current += 1
        return value


r = MyRange(1, 4)

for x in r:
    print(x)

for x in r:
    print(x)

这次两轮都会输出 1、2、3。

为什么。

因为每次 for x in r 时,都会调用 r.__iter__()
而这个方法每次都会创建一个全新的 MyRangeIterator。
也就是说,每次遍历都是全新起点。

这就是很多容器类的典型设计思路。

什么时候对象自己当迭代器,什么时候分开设计

这是实际开发里很重要的判断。

对象自己当迭代器,适合这些场景:

只打算一次性迭代
对象本身就是“流式消费器”
状态推进本来就是它的核心职责

比如:

文件读取器
生成器
网络流读取器
某些分页消费器

分开设计更适合这些场景:

对象代表的是一份数据集合
希望每次 for 都能从头开始
同一个对象可能被多个地方并行遍历

比如:

列表
元组
字符串
自定义容器类

所以不是哪种更高级,而是看对象更像“集合”,还是更像“流”。

自己实现一个班级点名器,最能体会分开设计的意义

看一个完整一点的例子:

class StudentIterator:
    def __init__(self, students):
        self.students = students
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= len(self.students):
            raise StopIteration

        student = self.students[self.index]
        self.index += 1
        return student


class ClassRoom:
    def __init__(self, students):
        self.students = students

    def __iter__(self):
        return StudentIterator(self.students)


room = ClassRoom(["张三", "李四", "王五"])

for name in room:
    print(name)

这个例子里:

ClassRoom 是可迭代对象
StudentIterator 是真正的迭代器

这样设计的好处是特别清楚:

班级本身代表一组学生
点名器代表当前点名进度

逻辑非常顺。
而且你还能多次从头点名,不会互相干扰。

迭代器最本质的能力,不是存数据,而是管理进度

这句话值得单独拎出来。

很多人一开始以为迭代器就是“换个方式存列表”。
其实不是。

迭代器最值钱的地方在于:

它不一定需要把所有数据都提前放好
它只需要知道:下一步怎么给值

比如倒计时迭代器,并没有存 [5, 4, 3, 2, 1] 这个列表。
它只是记住当前数字,每次按规则减 1。

MyRange 也没有提前生成所有值。
它只是记住 start、end 和 current。

所以迭代器真正强的地方是:

按需产出
保存状态
逐步推进

这也是它和普通容器最深层的区别之一。

StopIteration 不只是结束标志,它还是协议中最关键的一环

很多人第一次写迭代器时,容易想当然地返回 None 代表结束。
但这在协议里是不对的。

比如这样写就是错误思路:

def __next__(self):
    if 没有值了:
        return None

为什么不行。

因为 None 本身也可能是一个合法值。
for 循环无法仅靠 None 判断到底是“值就是 None”,还是“真的结束了”。

所以 Python 统一规定:
结束必须抛 StopIteration。

这是一个非常明确、不会混淆的信号。
for、list()、sum()、max() 等等很多工具,都是靠它判断遍历结束。

这就是协议的价值。
所有人都按同一个方式说“结束了”,整个生态才统一。

自己手动模拟 next,比盯着 for 更容易理解协议

比如前面的班级点名器:

room = ClassRoom(["张三", "李四", "王五"])
it = iter(room)

print(next(it))
print(next(it))
print(next(it))

输出:

张三
李四
王五

再来一次:

print(next(it))

就会抛 StopIteration。

这个手工过程特别值得多练几次。
因为它能让你明显感觉到:

迭代器不是“自动魔法”,而是一个很具体的对象
next 不是“取数组元素”,而是“请求迭代器交出下一项”
StopIteration 不是“异常事故”,而是“正常结束通知”

一旦把这些动作都看实,后面的生成器、yield 才不会学得飘。

迭代器和索引遍历,不是一回事

比如列表可以这样遍历:

nums = [10, 20, 30]

for i in range(len(nums)):
    print(nums[i])

这当然也能跑。
但它依赖什么。

依赖:

len(nums)
nums[i]

也就是说,它要求对象必须支持长度和下标访问。

而迭代协议完全不要求这些。
只要对象能提供下一个值,就能被 for 遍历。

这就是为什么文件对象、生成器、自定义流式对象能 for,但你很难用索引方式遍历它们。

所以从抽象层级上说:

索引遍历依赖“随机访问”
迭代遍历依赖“顺序产出”

后者更通用,也更符合 Python 的风格。

有些对象能被 list() 转成列表,本质上也是因为它们支持迭代协议

比如:

room = ClassRoom(["张三", "李四", "王五"])
print(list(room))

输出:

['张三', '李四', '王五']

为什么能这样。

因为 list() 做的事,本质上也是:

先 iter()
再不断 next()
直到 StopIteration

换句话说,for 不是唯一使用迭代协议的地方。
很多内置函数都建立在这套协议上。

比如:

list()
tuple()
set()
sum()
max()
min()
any()
all()

所以你一旦把迭代协议实现好,自定义对象就能自然接入很多 Python 工具。

写迭代器时,最容易犯的几个错误

第一个错误,忘了在结束时抛 StopIteration。

def __next__(self):
    if self.index >= len(self.data):
        return

这样写协议就不规范。
应该明确抛异常。

第二个错误,忘了推进状态。

def __next__(self):
    if self.index >= len(self.data):
        raise StopIteration
    return self.data[self.index]

这里没有 self.index += 1,结果就是每次都返回同一个值,for 会死循环。

第三个错误,边界条件写错。

比如把 >= 写成 >,就可能多取一个或者少取一个。
这类 off-by-one 错误在迭代器里非常常见。

第四个错误,把“可重复遍历对象”和“一次性迭代器”混在一起设计。
结果第一遍能用,第二遍突然没值,自己还没意识到为什么。

这些坑都不算高级,但非常常见。
真正写几次迭代器,就会对它们越来越敏感。

自己实现一个斐波那契迭代器,能更明显体会“动态产出”的意思

来看一个更有味道的例子。
生成前 n 个斐波那契数。

class Fibonacci:
    def __init__(self, count):
        self.count = count
        self.index = 0
        self.a = 0
        self.b = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= self.count:
            raise StopIteration

        if self.index == 0:
            self.index += 1
            return 0

        if self.index == 1:
            self.index += 1
            return 1

        self.a, self.b = self.b, self.a + self.b
        self.index += 1
        return self.b


for x in Fibonacci(8):
    print(x)

输出大概是:

0
1
1
2
3
5
8
13

这个例子特别有代表性。
因为它不是从现成列表里往外拿值,而是在迭代过程中不断计算下一个值。

这正说明:

迭代器并不等于“包装一个已有序列”
它完全可以“边走边算”

这也是惰性计算思想的基础之一。

但这个 Fibonacci 还有优化空间

现在这个版本既是可迭代对象,又是迭代器。
所以遍历一遍后,它就耗尽了。
如果你想让它支持重复遍历,可以照前面的思路拆开:

一个 Fibonacci 可迭代对象
一个 FibonacciIterator 专门维护 index、a、b 状态

这就是为什么理解“对象本身是不是迭代器”特别重要。
因为它直接决定对象的使用体验。

什么时候该自己实现迭代器

平时写业务代码,不是天天都要手搓迭代器。
但在这些场景里,它会很有价值。

第一,自定义容器想支持 for。
比如树节点集合、分页结果集、班级对象、消息列表包装器。

第二,想按规则动态生成值。
比如倒计时、区间数列、斐波那契、分页游标。

第三,想把“取下一个值”的复杂逻辑封装起来。
外部只负责 for,不用关心内部状态怎么推进。

第四,想接入 Python 原生的迭代生态。
比如让 list()、sum()、max() 等能直接吃你的对象。

你会发现,自己实现迭代器的核心价值,不是炫技术,而是让对象更自然地融入 Python。

用一个特别实用的判断方式收尾

以后你看到一个对象,或者想设计一个对象时,可以问自己三句:

它是不是一个集合,应该每次都能从头遍历
如果是,那通常让 __iter__ 返回一个新迭代器更合适

它是不是一个流,天生就是一次性消费
如果是,那对象自己做迭代器也可能合理

它的“下一个值”是不是需要根据状态一步步算出来
如果是,迭代器往往就是非常自然的方案

这三句问清,很多设计选择就明白了。

本章小结

实现一个迭代器的核心,就是写好 __iter____next__:前者返回迭代器,后者负责产出下一个值并在结束时抛出 StopIteration。对象本身可以直接充当迭代器,也可以把“可迭代对象”和“迭代器”拆开设计。前者适合一次性流式消费,后者更适合可重复遍历的容器。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐