news 2026/10/13 11:57:32

Python迭代器详解:从__iter__与__next__到for循环底层原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python迭代器详解:从__iter__与__next__到for循环底层原理

前几天有人问我:你写了这么多年Python,能不能不用for循环,把列表里的数字一个一个手动取出来?这个问题看着简单,但他真正想问的是——for循环背后到底发生了什么。很多人学了列表、字典、元组,却不知道它们之所以能用在for循环里,靠的是一套统一的“迭代协议”;而这套协议的核心,恰恰是两个看起来很神秘的魔法方法:__iter__和__next__。今天借这个“魔法方法:迭代器+习题复习”的整理,我把迭代器这条线从头到尾捋一遍,顺便带上习题和排错记录,保证你读完能真正弄懂迭代器,而不是停留在“会用for循环”的层面。

1. 迭代器到底是什么:先把三个名字的关系理清楚

1.1 可迭代对象、迭代器、迭代协议:别再用混了

不少初学Python的人会有一个典型误区:认为“能用for循环遍历的东西”就是迭代器。这句话错在哪?错在把“可迭代对象”和“迭代器”混成了一回事。

严格来说,三者是这么区分的:

  • 可迭代对象(Iterable):实现了__iter__()方法的对象,可以被iter()函数处理,返回一个迭代器。列表、元组、字典、集合、字符串都属于这一类。
  • 迭代器(Iterator):同时实现了__iter__()和__next__()方法的对象。__iter__()返回自己,__next__()每次返回下一个值,没有值可返回时抛出StopIteration异常。
  • 迭代协议:就是这两个魔法方法组合起来的一套规则,任何对象只要遵守这套规则,就能被for循环、列表推导式、in操作符等语法“通吃”。

我习惯用一个生活化的类比:可迭代对象相当于一个图书馆,迭代器相当于你手里的借书单。图书馆本身不是书,但你可以通过借书单一本一本地把书拿出来;借书单上的进度记录了你看到哪一本了。每次调用next(),就好比“翻到下一页”,看到最后一页之后再翻就会报错——这就是StopIteration。

1.2 for循环的底层流程:一行代码背后的三步走

当你在Python里写:

for item in my_list: print(item)

解释器并不会“神奇地知道”my_list有哪几个元素,它实际执行的是下面这套动作:

  1. 调用iter(my_list),尝试调用对象的__iter__()方法,拿到一个迭代器对象。
  2. 反复调用next(iterator),也就是调用迭代器的__next__()方法,拿到一个值赋给item。
  3. 当__next__()抛出StopIteration时,for循环捕获这个异常,安静地结束循环。

如果想亲眼验证这个过程,可以直接用内置函数模拟:

my_list = [10, 20, 30] it = iter(my_list) while True: try: value = next(it) print(value) except StopIteration: break

这段代码跟for value in my_list的输出完全一致。理解了这一步,你就明白为什么迭代器是“一次性”的:它内部维护着当前进度,走完就不会回头。列表这种可迭代对象可以重新生成新的迭代器,所以可以反复遍历;而迭代器本身一旦被next()走到头,再想循环就空了。

注意:以后看到“可迭代对象”和“迭代器”,先在心里问一句:这东西传给iter()会返回什么?如果返回的是它自己,那就是迭代器;如果返回的是一个新对象,那它只是可迭代对象。

2. 魔法方法的核心细节:__iter__与__next__的规矩

2.1 方法签名与返回值:不是随便写就能跑

魔法方法之所以叫“魔法”,不是因为有什么黑科技,而是因为解释器在特定语法场景下会自动调用它们。你不需要显式写obj.__iter__(),写iter(obj)就够了;你不需要手动捕获StopIteration,for循环已经帮你兜底了。但方法本身的实现要守规矩:

  • __iter__(self):必须返回一个迭代器对象。常见做法是return self,前提是这个类自己也实现了__next__()。
  • __next__(self):必须返回序列中的下一个元素。当没有元素可返回时,必须抛出StopIteration异常,而不是返回None或返回一个默认值。
  • __iter__和__next__都要定义。如果只定义__next__、忘了__iter__,那这个对象就不能被iter()识别,for循环会直接报TypeError: 'xxx' object is not iterable。

有一个常见的误区:有人会在__next__里用return None表示结束。这看起来“程序没报错”,但后果很严重——for循环会继续调用__next__,然后变成无限循环,因为它永远等不到StopIteration。记住:StopIteration不是说“返回一个空值”,而是“迭代到此结束”的信号,两者完全不是一回事。

2.2 一个容易被忽略的退路:只实现__getitem__也能迭代

很多人不知道,迭代协议其实有两条路。除了__iter__+__next__这条“主路”,解释器还保留了一条“老路”:如果你只实现了__getitem__,并且它支持从0开始的整数索引,那么iter()函数也能工作。它会在内部生成一个“老式迭代器”,按0、1、2、3...的顺序不断调用__getitem__,直到索引越界抛出IndexError为止。

我见过不少老代码、甚至某些第三方库里的类只写了__getitem__,没写__iter__,却依然能用在for循环里,原因就在这里。但我要提醒你:这只是为了向后兼容的退路,新写的代码不要依赖它。原因很简单:__getitem__的语义是“按索引访问”,这种访问方式天然要求数据支持随机存取;而迭代器的核心价值是“按顺序逐个产出”,两者思维模型不一样。如果你实现的是流式数据、生成序列,老老实实按主路走会更清晰。

2.3 迭代器的“一次性”特性:为什么同一个迭代器不能循环两次

这个坑几乎每个Python开发者都踩过。请看这段代码:

nums = [1, 2, 3] it = iter(nums) print(list(it)) # [1, 2, 3] print(list(it)) # []

第一次list(it)把迭代器从头到尾消费完了,第二次自然什么都得不到。但如果你写成:

nums = [1, 2, 3] print(list(nums)) print(list(nums))

两次输出都是[1, 2, 3],因为列表每次都会创建一个新的迭代器。

这个特性在生产代码里经常引发“诡异的bug”。我有一个印象很深的案例:某同事用sorted(reader)处理一个大文件的读取迭代器,第一次读取时数据正常,第二次处理同一个迭代器时,sorted返回了空列表。排查半天才发现问题不是数据没了,而是迭代器被第一次的排序操作消费掉了。解决方案也很简单:要么每次都新建迭代器,要么把结果先存成列表。

3. 亲手写一个迭代器:从零到能跑

3.1 实现一个按步长取数的迭代器类

光看不练假把式,我们直接写一个自定义迭代器。下面这个StepRange类,功能类似一个“支持自定义步长、且生成过程完全可控”的数字序列:

class StepRange: """每次从start跳到stop,步长为step的迭代器""" def __init__(self, start, stop, step=1): self.current = start self.stop = stop self.step = step def __iter__(self): return self def __next__(self): if self.current >= self.stop: raise StopIteration result = self.current self.current += self.step return result

测试一下:

sr = StepRange(0, 5, 1) for x in sr: print(x, end=' ') # 0 1 2 3 4 print(list(StepRange(0, 10, 2))) # [0, 2, 4, 6, 8]

代码很简单,但有几个细节值得琢磨:

  • 为什么__next__里先判断current >= stop再返回值?因为我们要保证“当前值达到边界时不输出”,这与range的右开区间行为一致。
  • 为什么要先把result保存下来,再更新current?因为一旦更新了current,原有的值就丢了。顺序写反,输出就会错位。
  • 为什么__iter__``return self?因为迭代器本身就是“能产生下一个值的对象”,它就是自己的迭代器。

3.2 斐波那契迭代器:理解“可暂停”和“可恢复”

接下来写一个稍微实用一点的例子:斐波那契数列迭代器。斐波那契数列天然适合迭代器,因为它的生成过程是“从前面两项推后面一项”,不需要一次性算几千项存进列表。这个特性叫“惰性求值”:需要多少,就算多少。

class Fibonacci: def __init__(self, max_count=None): self.max_count = max_count self.count = 0 self.a, self.b = 0, 1 def __iter__(self): return self def __next__(self): if self.max_count is not None and self.count >= self.max_count: raise StopIteration result = self.b self.a, self.b = self.b, self.a + self.b self.count += 1 return result

这个类最值得体会的一点是“状态保存”。每次__next__()被调用,它只做三件事:保存当前值、推进状态、计数。调用next()之后,对象内部的状态已经变了,但外部看起来就像“暂停了一下”;下次再调用时,从暂停的地方继续。这跟生成器函数的工作方式本质相同。

顺便对比一下生成器的写法:

def fibonacci_gen(max_count=None): a, b = 0, 1 count = 0 while max_count is None or count < max_count: yield b a, b = b, a + b count += 1

能看到yield关键词把函数变成了生成器,你要做的事情被大幅简化。很多人在学迭代器时会问:那我还需要手写迭代器类吗?答案是:大多数场景不需要,生成器是更优先的选择;但当你需要实现一个同时具备“迭代”和“其他协议”的复杂类时,理解手写迭代器仍然很关键。

3.3 可迭代对象与迭代器为什么要分开设计

前面写的StepRange和Fibonacci都是“自己就是自己的迭代器”,这在简单场景下没问题。但有一个明显的缺陷:它们是“一次性”的,同一个实例不能被重复遍历。如果业务上需要“同一个容器对象可以反复for循环”,那就得采用“容器和迭代器分离”的设计。

来看这个例子,自定义一个单词列表类:

class WordList: def __init__(self, words): self.words = words def __iter__(self): return WordListIterator(self) class WordListIterator: def __init__(self, word_list): self.words = word_list.words self.index = 0 def __iter__(self): return self def __next__(self): if self.index < len(self.words): result = self.words[self.index] self.index += 1 return result raise StopIteration

使用:

wl = WordList(["hello", "world", "python"]) print(list(wl)) # ['hello', 'world', 'python'] print(list(wl)) # ['hello', 'world', 'python'],可以重复遍历 it1 = iter(wl) it2 = iter(wl) print(next(it1)) # hello print(next(it2)) # hello,两个迭代器互不干扰

这种设计的好处是:可迭代对象负责“描述数据容器长什么样”,迭代器负责“记录当前遍历到哪个位置”。多个迭代器同时存在时,它们各自维护各自的进度,互不干扰。很多标准库对象就是这么设计的。你回想一下:一个文件对象既是可迭代对象也是迭代器,所以读取文件时如果两个地方同时用同一个文件对象迭代,会互相影响;而enumerate每次都会生成一个新的迭代器。

4. 习题复习:从易到难,带答案解析

4.1 基础判断题:分辨哪些会报错

下面这几个表达式,哪些能正常运行?哪些会抛异常?

# 场景1 nums = [1, 2, 3] it = iter(nums) next(it) # ? next(it) # ? next(it) # ? next(it) # ? # 场景2 s = "abc" it = iter(s) next(it) # ? # 场景3 n = 10 iter(n) # ?

答案解析:

场景1中,前三个next(it)依次返回1、2、3,第四个next(it)抛出StopIteration。很多初学者会误以为“取完最后一个元素后,再取会返回None”,这是错的。迭代器讲究“到了尽头就明确告诉你:没有了”,而不是静默返回一个值。

场景2能正常运行,第一次next(it)返回'a'。字符串也是可迭代对象,因为字符串实现了__iter__。这一点常被忽略,但在处理字符序列、正则匹配时非常有用。

场景3直接报TypeError。整数不是可迭代对象,它没有__iter__方法,也不具备__getitem__老式迭代的条件。这个判断方法很有用:如果某个对象既没有__iter__,也没有适配整数索引的__getitem__,那它一定不能迭代。

4.2 动手写代码题:实现一个返回前n个偶数的迭代器类

这道题是面试官很爱出的热身题,考察的是你能否区分“方法实现”和“业务逻辑”。

class EvenNumbers: def __init__(self, n): self.n = n self.count = 0 self.current = 0 def __iter__(self): return self def __next__(self): if self.count >= self.n: raise StopIteration self.current += 2 self.count += 1 return self.current

测试:

e = EvenNumbers(4) print(list(e)) # [2, 4, 6, 8]

这里有个小陷阱:self.count >= self.n的判断放在最前面,是为了保证“如果n是0,一次都不产出”。如果把它放在更新之后,那n=0时也会返回一个2,逻辑就错了。写迭代器时,边界条件的判断顺序很关键,顺序错了会多出一个元素。

4.3 陷阱排查题:为什么第一个for循环正常,第二个什么都没打印

来看这段代码:

class Mystery: def __init__(self, data): self.data = data def __iter__(self): return iter(self.data) m = Mystery([1, 2, 3]) for x in m: print(x) for x in m: print(x)

运行结果:

1 2 3

第二次循环什么都没输出。为什么?

因为Mystery.__iter__返回的是iter(self.data)——它每次都会创建一个新的列表迭代器。等等,如果是每次创建新的,那第二次应该也能正常输出才对?问题出在:我上面这段代码的__iter__确实是返回新迭代器,所以理论上两次循环都应该正常。但实际很多人在写__iter__时,会写成类似下面这种错误版本:

def __iter__(self): return iter(self)

这会导致无限递归,因为iter(self)又会调用__iter__本身。而如果类里有个属性叫self.iterator,并且在__iter__里return self.iterator,那就会“一次性”问题。

其实这类题目真正的坑点在于:迭代器对象被保存成实例属性。看这个版本:

class Mystery: def __init__(self, data): self.data = data self.iterator = iter(self.data) def __iter__(self): return self.iterator

第一次for循环把self.iterator消费完了,第二次自然输出为空。这个案例再次说明:可迭代对象与迭代器分离设计不是学院派建议,而是真实工程里的必要手段。

4.4 进阶思考题:如何实现一个“可重置”的迭代器

日常业务里有这么一种需求:迭代器被消费到一半,因为某个条件要回溯到开头重来。但迭代器本质是“向前走,不回头”,所以直接改造__next__没有意义。更合理的做法是让类既提供迭代协议,又提供一个reset方法:

class ResettableIterator: def __init__(self, data): self.data = data self.index = 0 def __iter__(self): return self def __next__(self): if self.index >= len(self.data): raise StopIteration result = self.data[self.index] self.index += 1 return result def reset(self): self.index = 0

用法:

ri = ResettableIterator([5, 6, 7, 8]) print(list(ri)) # [5, 6, 7, 8] ri.reset() print(next(ri)) # 5

这个设计比“重新创建一个新迭代器”更灵活的地方是,它保留了对象上的其他状态和数据。比如你正在遍历一个日志文件,想从头再读一遍,就可以保留文件句柄,只重置游标。不过要注意:如果迭代器内部状态非常复杂,reset会很难维护,这时不如直接丢掉旧迭代器、创建新的。

5. 常见问题与排查技巧实录

5.1 迭代器报错的五个典型场景

下面这五个场景,我基本每一两个季度就会遇到一次,整理成速查表方便收藏:

场景现象原因解决方案
for循环拿不到值循环体一次都没执行迭代器已被之前某处消费完检查是否复用了同一个迭代器
出现无限循环程序卡死不动,CPU占用飙升__next__里用了return None而不是raise StopIteration必须显式抛出停止异常
报错'xxx' object is not iterable把不可迭代对象传给iter()或for循环类没有实现__iter__,也没有__getitem__退路给类添加__iter__方法
遍历列表时修改列表RuntimeError: dictionary changed size during iteration在遍历过程中增删元素先复制副本list(origin)再遍历,或使用filter
__iter__返回了Nonefor循环报TypeError: iter() returned non-iterator方法里忘记return self或返回了错误对象检查返回值是否实现了__iter__和__next__

5.2 排查技巧:怎么快速确认一个对象是“可迭代对象”还是“迭代器”

在实际调试中,我经常要用两个小检查:

from collections.abc import Iterable, Iterator # 检查是否为可迭代对象 isinstance(obj, Iterable) # 检查是否为迭代器 isinstance(obj, Iterator)

但请注意:Iterator继承自Iterable,所以一个迭代器一定同时满足两个检查。反过来,一个列表是可迭代对象,但不是迭代器。拿内置类型验证一下:

lst = [1, 2, 3] print(isinstance(lst, Iterable)) # True print(isinstance(lst, Iterator)) # False it = iter(lst) print(isinstance(it, Iterable)) # True print(isinstance(it, Iterator)) # True

另外一个排查经验是:当一段代码“第一次运行正常,第二次结果为空”时,优先怀疑迭代器被复用。特别是涉及文件对象、生成器、map、filter、zip这类惰性对象时,它们都是迭代器,只能消费一次。如果一定要跑两遍数据,最笨也最稳的办法是:第一遍消费完后,把结果list()保存到内存,后续操作都基于列表进行。

5.3 生成器函数与迭代器类:怎么选才顺手

生成器(yield)本质是“用函数语法快速创建迭代器”。它比手写迭代器类省事太多,所以大部分场景我推荐优先用生成器。那什么时候该用手写迭代器类?

  • 需要同时实现多个协议,不只是迭代,比如还要支持__len__、__getitem__、__enter__/__exit__。
  • 需要精细控制内部状态,或者状态字段很多,函数式写法难以表达。
  • 需要实现reset这类非标准操作,生成器没法直接做到。
  • 需要可重复遍历的容器类(分离设计),这时候生成器只能生成“一次性迭代器”。

从多年实战看,80%的需求用生成器就够,但20%的复杂类还是要手写迭代器。如果面试遇到手写迭代器题,可以先写生成器版本,再说明哪种场景必须手写类,这个回答会让人对你刮目相看。

6. 迭代器在真实项目里的三个延伸方向

6.1 大文件流式处理:一次只读一行,内存不爆

生产环境里经常要处理几个GB的日志文件。如果直接read()全部读进内存,服务器分分钟挂给你看。正确姿势是利用文件对象本身就是迭代器这个特性:

with open("big_log.txt", "r", encoding="utf-8") as f: for line in f: # 处理这一行 process(line)

这里的for line in f之所以不会把整个文件加载进内存,就是因为文件对象内部实现了按行读取的迭代逻辑。每次next()只从磁盘读一小块数据,处理完就丢弃,内存占用始终保持在一个很小的范围内。这个特性看起来平淡,但它解决的是“大数据量下程序能不能跑起来”的生死问题。

6.2 与itertools配合:无限序列的“可控消费”

itertools是Python标准库里的迭代器工具箱,专门为迭代器提供各种加工函数。比如islice可以“切出”一个无限迭代器的前几个值:

from itertools import islice def count_from(n): while True: yield n n += 1 for value in islice(count_from(10), 5): print(value, end=' ') # 10 11 12 13 14

不懂迭代器的人看到while True会以为程序死循环了,实际上这里有个关键的逻辑:无限生成器本身是惰性的,谁也不会主动去取它的值,只有islice要求取5个时它才产生5个数据。这是“惰性求值”的典型应用。工作中处理流水号、请求重试序号、分页游标时,这种组合非常实用。

6.3 自定义分页器:把迭代协议用到业务代码里

我之前处理过一个分页查询需求:数据库里有大量记录,希望按每页50条逐页取数,同时支持按业务时间范围过滤。如果用列表一次性取出全部数据,数据一多就会导致接口响应变慢。于是我把分页逻辑封装成一个迭代器类,核心思路是:每次__next__从数据库取下一页,处于“取完为止”的惰性计算状态。

这个类没有特别复杂,但它把“业务中的分页状态”和“迭代协议”结合了起来,调用方只需要写for page in Paginator(...):就能一路翻页。这种“让业务对象自己具备迭代能力”的写法,比起在业务代码里到处写while offset < total清晰得多。

7. 踩坑实录与最终经验

我自己在迭代器上栽过最狠的一次跟前面说的“迭代器被提前消费”有关。当时在一个批处理脚本里,我把filter(func, data)的结果传给两个下游函数用,第一个函数先遍历了一遍,第二个函数拿到的是空集合。折腾了一个下午,最后发现问题出在filter返回的是迭代器,不是列表。从那以后我养成一个习惯:只要把一个生成器/迭代器的结果传给多个消费者,先问一句“这个东西会被消费几次?”,不确定时就先转成列表。

还有一个小技巧值得分享:调试迭代器时,别在循环里打日志,更别在__next__里打日志。因为__next__每被调用一次就打印一次,数据一多日志直接泛滥;而且for循环会提前尝试获取下一个元素来判断是否结束,日志顺序会跟你预期的完全不一样。我一般用list(it)先把迭代器转成列表,再整体观察输出。

最后一个建议:学迭代器的时候一定动手做一遍习题。纸上谈兵回答“我知道迭代器是一次性的”,和实际调通一个自定义迭代器类,是两种完全不同的掌握程度。迭代器是Python里为数不多“用一个魔法方法就能改变整个对象行为模式”的机制,吃透它,你再去看生成器、协程、上下文管理器,都会觉得顺理成章。今天写到这里,如果你也遇到过什么迭代器的诡异问题,欢迎在评论区留下你的案例,一起排查。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 11:57:21

MSCNN人群密度估计:多尺度融合与损失函数实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 11:56:56

Hadoop伪分布式环境搭建与Linux底层排错实战

简介&#xff1a;本资源是面向大数据初学者的入门级实验指导文档&#xff0c;聚焦Linux基础环境搭建与Hadoop伪分布式实践&#xff0c;适用于高校大数据课程实验、自学入门及实训教学场景。文档系统覆盖三大核心能力&#xff1a;Linux虚拟机&#xff08;Ubuntu 16.04/18.04&…

作者头像 李华
网站建设 2026/10/13 11:56:40

STM32软件SPI驱动ST7735:引脚分配、初始化与刷屏优化

简介&#xff1a;这份资源面向使用STM32F103C8T6进行嵌入式显示开发的初学者与进阶开发者&#xff0c;提供基于HAL库、以软件SPI方式驱动1.8寸ST7735 TFT LCD彩屏的完整工程。它解决了硬件SPI资源紧张或引脚受限时如何点亮彩屏的问题&#xff0c;涵盖GPIO模拟时钟、MISO、MOSI与…

作者头像 李华
网站建设 2026/10/13 11:55:59

CAN总线通信从入门到实战:仲裁机制、物理层与调试技巧详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 11:55:49

命名实体识别NER全攻略:序列标注、BIOES与BiLSTM-CRF实战笔记

2021年秋招&#xff0c;我在准备NLP算法岗的面试题时&#xff0c;几乎每轮都会碰到同一个话题&#xff1a;NER。命名实体识别作为NLP基础任务模型里的典型代表&#xff0c;它考察的其实不只是背几个概念&#xff0c;而是一个人能否把数据、标签、模型、评估和部署串成一条完整链…

作者头像 李华
网站建设 2026/10/13 11:53:46

学习平台前后端分离实践:SpringBoot+Vue+MyBatis架构详解

1. 为什么我把智能学习平台拆成前后端分离两套工程去年上半年我接手了一个学习类项目&#xff0c;需求说简单也简单&#xff1a;管理员能上传课件和习题&#xff0c;学生能在线刷题、看学习记录&#xff0c;老师能批改作业、发布通知。但真做起设计时我发现&#xff0c;如果还用…

作者头像 李华