在Python中,内置的range类广为人知,它产生的对象可以被for循环多次遍历,且每次都是从起点重新开始。如果我们想自己实现一个类似的工具,比如生成指定区间内的平方数序列,并且希望它也能反复使用,就需要理解可重用迭代器的设计方式。很多初学者会用生成器函数,但生成器函数返回的迭代器是一次性的,遍历完就枯竭了。本文将通过定义一个类来模拟range的行为,使其支持重复迭代。

为什么生成器无法复用
生成器函数使用yield语句暂停并输出值,每次调用该函数会返回一个全新的生成器对象,但这个对象内部维护了自身的执行位置。一旦通过for循环或next函数走到函数末尾,生成器就进入了枯竭状态,再次迭代不会产生任何元素。下面的代码展示了这一限制:
def my_range_gen(start, stop):
current = start
while current < stop:
yield current
current += 1
gen = my_range_gen(0, 3)
print(list(gen)) # 输出 [0, 1, 2]
print(list(gen)) # 输出 [],生成器已耗尽
从上面的例子可以看出,第二次调用list(gen)得到的是空列表。这是因为生成器对象本身保存了current变量的状态,函数执行结束后状态不可重置。如果我们在多个地方需要同一序列,用生成器就不得不反复调用函数创建新对象,当序列构造代价较高时很不划算。
与之相反,range类的实例并不保存迭代进度,它只保存起始、终止和步长等描述信息。每次有人对它使用iter()时,Python都会根据这些信息新建一个独立的迭代器。这种分离让range对象本身保持轻量且可重用,我们也应当用同样的思路来设计自定义类。
实现可重用迭代器的核心协议
要让一个类既自身可迭代,又能每次给出全新的遍历进度,需要同时实现两个方法:__iter__和__next__。通常做法是让类自身作为可迭代对象,再由__iter__返回另一个专门记录进度的迭代器类实例。这样原对象可以多次调用__iter__而互不干扰。
class RangeLike:
def __init__(self, start, stop, step=1):
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
# 每次返回新的迭代器,保证可重用
return RangeIterator(self.start, self.stop, self.step)
class RangeIterator:
def __init__(self, start, stop, step):
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
在上面的代码中,RangeLike只负责保存配置,__iter__方法每次被调用都会构造一个RangeIterator。RangeIterator自己也是可迭代的(因为它也有__iter__返回自身),同时实现了__next__来按步长推进。由于每次for循环都会触发RangeLike的__iter__,因此即便多次遍历,彼此的current值也互不影响。
这种结构清晰地划分了职责:容器类描述数据范围,迭代器类管理遍历状态。比起把状态直接堆在生成器里,它更符合面向对象设计,也方便以后扩展,比如增加反向迭代或者动态计算值的功能。
验证复用行为与内置range的相似性
我们可以用同一实例进行多次循环,观察是否每次都从起点开始。同时对比内置range,确认行为一致。
rl = RangeLike(1, 4) print(list(rl)) # [1, 2, 3] print(list(rl)) # [1, 2, 3],可重用 r = range(1, 4) print(list(r)) # [1, 2, 3] print(list(r)) # [1, 2, 3]
测试结果表明,自定义的RangeLike和内置range一样,重复调用list都能得到完整序列。如果业务里需要生成复杂序列(例如斐波那契数段、平方数),只需修改RangeIterator的__next__计算逻辑,而不用改动外层调用方式。
此外,这种实现也兼容在for循环里使用,以及通过手动next()逐步取值。当序列较大时,由于不预先存储所有元素,而是按需计算,内存占用和range一样是常量的,非常适合处理大区间。
增加步长校验与负步长支持
内置range支持负步长,例如range(5, 0, -1)会输出5、4、3、2、1。我们也可以在__iter__里根据步长正负决定迭代器的比较方向,让自定义类更完善。
class RangeLikeV2:
def __init__(self, start, stop, step=1):
if step == 0:
raise ValueError('step不能为0')
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
return RangeIteratorV2(self.start, self.stop, self.step)
class RangeIteratorV2:
def __init__(self, start, stop, step):
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if self.step > 0 and self.current >= self.stop:
raise StopIteration
if self.step < 0 and self.current <= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
# 测试负步长
rv = RangeLikeV2(5, 0, -1)
print(list(rv)) # [5, 4, 3, 2, 1]
print(list(rv)) # 再次调用仍输出 [5, 4, 3, 2, 1]
上述版本在初始化时排除了步长为零的非法情况,并在迭代器中分别处理正向和反向终止条件。这样,不论外界以何种步长创建对象,实例都可安全复用,且逻辑与内置range对齐。
通过把迭代协议拆成可迭代类与迭代器类,我们既获得了类似range的复用能力,又保留了自定义计算空间的自由度。在写工具库或数据处理模块时,这种手法能显著减少重复对象构建成本,也让代码意图更加直白。