在Python中,自定义类如果想被用于for循环、元组解包或者推导式,就需要遵守迭代协议。很多初学者以为只要类里能按索引取数据就够了,但实际上解释器对可迭代对象有一套明确的识别规则。只有搞清楚这套规则,才能写出行为正确的类。

一、迭代协议的基础原理
Python的迭代协议分为两个角色:可迭代对象(iterable)和迭代器(iterator)。可迭代对象必须提供__iter__方法,该方法返回一个迭代器。迭代器则必须实现__next__方法,并在没有元素时抛出StopIteration异常,同时迭代器自身通常也实现__iter__返回自己。
当执行for x in obj时,解释器首先调用iter(obj),也就是obj.__iter__()获取迭代器;然后不断调用next()推进。如果对象没有__iter__但实现了__getitem__,旧式兼容逻辑会尝试从索引0开始取下标,直到抛出IndexError。这种退化方式不支持多次独立遍历,也容易在其它语法中失效。
二、常见错误写法与隐患
下面这段代码模拟了一个范围类,但只实现了__getitem__。它在简单for循环中看似可用,却不是严格意义上的可迭代对象。
class WrongRange:
def __init__(self, stop):
self.stop = stop
def __getitem__(self, index):
if index >= self.stop:
raise IndexError
return index
# 看似能循环
for i in WrongRange(3):
print(i)
这种写法的问题在于:它无法被iter()正常识别为可迭代对象(在部分检查中会被拒绝),且每次想重新遍历都得依赖下标退化机制。如果类内部维护了状态,退化访问还会导致逻辑混乱。此外,在解包或作为函数参数传递时,行为可能不符合预期。
三、正确的自定义可迭代对象
正确姿势是单独定义迭代器类,并在容器类的__iter__中返回它。这样每次调用__iter__都得到独立的遍历状态。
class RangeIterator:
def __init__(self, stop):
self.stop = stop
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
val = self.current
self.current += 1
return val
class RightRange:
def __init__(self, stop):
self.stop = stop
def __iter__(self):
return RangeIterator(self.stop)
# 可多次独立遍历
r = RightRange(3)
print(list(r))
print(list(r))
上面代码中,RightRange是可迭代对象,RangeIterator是迭代器。每次__iter__调用都新建一个迭代器,因此两次list(r)都能从头输出。这种结构清晰分离了容器与遍历状态,也完全符合Python语法在各种场景下的预期。
四、用生成器简化实现
如果觉得写迭代器类太繁琐,可以使用生成器函数。包含yield的函数被调用时返回的就是一个实现了迭代协议的对象,无需手动处理StopIteration。
class GenRange:
def __init__(self, stop):
self.stop = stop
def __iter__(self):
current = 0
while current < self.stop:
yield current
current += 1
# 使用方式完全一致
for i in GenRange(3):
print(i)
生成器方案减少了样板代码,同时保留了每次调用__iter__返回新遍历状态的特性。在绝大多数自定义可迭代对象的场景中,用__iter__配合yield是最推荐的写法,既正确又易读。
五、总结与检查清单
要确认你的类是可迭代对象,可以记住几点:类中是否定义了__iter__并返回迭代器;迭代器是否定义了__next__或在生成器中用yield;多次遍历是否互相独立。只要满足这些,你的对象就能在for、推导式、解包中稳定工作。
避免只依赖__getitem__的退化兼容,那是历史遗留路径。采用明确的迭代协议,不仅让代码更规范,也防止了状态共享带来的隐藏缺陷。