迭代器协议
迭代器是 Python 中实现惰性遍历的核心机制。理解迭代器协议,就能理解 for 循环背后的工作原理,也能让自己的类支持被 for 循环遍历。
可迭代对象与迭代器的区别
可迭代对象(Iterable)是指实现了 __iter__() 方法的对象,该方法返回一个迭代器。迭代器(Iterator)则是实现了 __iter__() 和 __next__() 的对象:__iter__() 返回自身,__next__() 每次返回序列中的下一个元素,当元素耗尽时抛出 StopIteration。
from collections.abc import Iterable, Iterator
# 列表是可迭代对象,但不是迭代器
staff = ["Alice", "Bob", "Charlie"]
print(isinstance(staff, Iterable)) # True
print(isinstance(staff, Iterator)) # False
# iter() 将可迭代对象转换为迭代器
it = iter(staff)
print(isinstance(it, Iterator)) # True
print(it is iter(it)) # True,迭代器的 __iter__ 返回自身
这个区别至关重要:可迭代对象可以反复创建新的迭代器来遍历,而迭代器本身是一次性的——一旦 __next__() 抛出 StopIteration,它就无法再次使用。
it = iter([1, 2, 3])
print(list(it)) # [1, 2, 3]
print(list(it)) # [],迭代器已耗尽
iter() 与 next()
iter() 是调用对象 __iter__() 方法的内置函数。next() 则调用迭代器的 __next__() 方法,并在元素耗尽时抛出 StopIteration。
s = "abc"
it = iter(s)
print(next(it)) # 'a'
print(next(it)) # 'b'
print(next(it)) # 'c'
print(next(it)) # StopIteration
next() 还支持第二个参数作为默认值。当迭代器耗尽时,返回默认值而不是抛出异常。这在处理长度不确定的序列时非常有用。
it = iter([1, 2])
print(next(it, "没了")) # 1
print(next(it, "没了")) # 2
print(next(it, "没了")) # "没了"
print(next(it, "没了")) # "没了",不会抛异常
for 循环的工作原理
for 语句本质上是对迭代器协议的语法糖。当执行 for x in iterable: 时,Python 在背后执行以下操作:
- 调用
iter(iterable)获取迭代器。 - 反复调用
next(iterator),将返回值赋给循环变量x。 - 当
next()抛出StopIteration时,捕获该异常并结束循环。
# 以下两种写法等价
for char in "abc":
print(char)
# 手动展开
_it = iter("abc")
while True:
try:
char = next(_it)
except StopIteration:
break
print(char)
StopIteration 是迭代器协议的信号,不是真正的错误。它只应在迭代器的 __next__() 方法内部被抛出,绝不应该在生成器或普通函数中手动捕获或抛出——Python 3.7+ 中,生成器内部抛出的 StopIteration 会被转换为 RuntimeError,以防止混淆。
自定义迭代器类
为类添加迭代器行为只需实现两个方法:__iter__() 返回一个带有 __next__() 的对象。如果类本身实现了 __next__(),__iter__() 可以直接返回 self。
class Reverse:
"""反向遍历序列的迭代器。"""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index -= 1
return self.data[self.index]
rev = Reverse("spam")
for char in rev:
print(char) # m, a, p, s
这个迭代器的状态保存在实例变量 self.index 中。每次调用 __next__() 时,索引递减并返回对应元素。当索引归零时抛出 StopIteration,通知消费者序列已耗尽。
独立迭代器 vs 可迭代对象
一个好的设计模式是:让可迭代对象和迭代器分离。可迭代对象每次调用 __iter__() 都返回一个全新的迭代器,支持多次遍历。
class Countdown:
"""可多次遍历的倒计时可迭代对象。"""
def __init__(self, start):
self.start = start
def __iter__(self):
# 每次返回新的迭代器,而不是 self
return CountdownIterator(self.start)
class CountdownIterator:
"""真正的迭代器,负责维护遍历状态。"""
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
num = self.current
self.current -= 1
return num
cd = Countdown(3)
print(list(cd)) # [3, 2, 1, 0]
print(list(cd)) # [3, 2, 1, 0],可以再次遍历
如果 Countdown.__iter__() 返回 self 并自己维护 current 状态,第二次遍历就会从 -1 开始,直接结束。分离设计让可迭代对象保持"纯净",迭代器承担"有状态"的职责。
迭代器的惰性特性
迭代器的核心价值在于惰性求值:它一次只产生一个元素,而不是一次性把所有元素加载到内存中。这使得处理大规模数据流成为可能。
def large_range(n):
"""返回 0 到 n-1 的迭代器,不占用 O(n) 内存。"""
i = 0
while i < n:
yield i
i += 1
# 处理 10 亿个数字,内存中只存一个整数
for num in large_range(1_000_000_000):
if num > 5:
break
文件对象本身就是迭代器,逐行读取文件时不会一次性加载全部内容:
with open("large.log") as f:
for line in f: # f 是迭代器,每次读一行
if "ERROR" in line:
print(line.strip())
迭代器协议的边界情况
- 空迭代器:
__next__()第一次调用就抛出StopIteration。 - 无限迭代器:永远不抛出
StopIteration,如itertools.count()。 - 有副作用的迭代器:
__next__()可能修改内部状态或外部资源,如从网络流读取数据。
class LineReader:
"""逐行读取并自动跳过空行的迭代器。"""
def __init__(self, lines):
self.lines = iter(lines)
def __iter__(self):
return self
def __next__(self):
while True:
line = next(self.lines)
stripped = line.strip()
if stripped:
return stripped
raise StopIteration
text = [" ", "hello", "", "world", " "]
reader = LineReader(text)
print(list(reader)) # ['hello', 'world']