《Python 高阶教程》018|自己实现一个迭代器:看懂 **iter** 和 **next**
上一章讲清了 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。对象本身可以直接充当迭代器,也可以把“可迭代对象”和“迭代器”拆开设计。前者适合一次性流式消费,后者更适合可重复遍历的容器。
更多推荐


所有评论(0)