深入解析Python __next__:迭代器协议核心与避坑实操

发布时间:2026/9/8 16:55:21
深入解析Python __next__:迭代器协议核心与避坑实操 说起来有点惭愧我见过不少写了好几年 Python 的人for循环用得飞起但你问他for x in obj这一步到底发生了什么他第一反应是“就是遍历嘛”。再追问一句如果让你手写一个支持for的类你会先实现哪个魔术方法很多人会愣住然后答__iter__。没错但只答对了一半。__iter__只是把对象变成可迭代对象的入口真正一个一个把值吐出来、决定什么时候停下来的是今天这篇的主角——__next__。在 Python 3.12 的魔术方法体系里第 86 篇轮到它恰好也是我认为整个迭代器协议里最容易被写错、又最值得琢磨的一个方法。这篇文章适合两类人一类是刚开始系统过 Magic Methods、对着官方文档不知道从哪下手的初学者另一类是已经会写迭代器但被迭代一次就空了第二次 for 不出东西为什么异常被吞了这类问题坑过的老手。我会把原理、完整代码、踩坑排查都放在一起争取你看完能直接拿去用。1. for 循环背地那个 whilenext() 到底在找谁的next先把最容易被忽略的事实摆出来for循环本质上是一个while循环它内部靠异常来控制结束。你写的for item in container: print(item)CPython 实际干的事情等价于下面这段展开逻辑iterator iter(container) # 调用 container.__iter__() while True: try: item next(iterator) # 调用 iterator.__next__() except StopIteration: break print(item)注意看第一行iter(container)拿到的是一个迭代器紧接着的next(iterator)才会触发对应的__next__。在很多人的直觉里for是直接遍历集合本身其实中间隔着一层迭代器。class Demo: def __iter__(self): return iter([1, 2, 3])你只能写for x in Demo()却不能直接next(Demo())因为Demo这个实例没有__next__方法。这说明在迭代器协议里一个完整角色必须同时具备两个能力返回迭代器、产生下一个值。前者是__iter__后者是__next__。Python 官方文档对object.__next__(self)的定义非常简短返回迭代器的下一个项目如果没有更多项目则抛出StopIteration异常。就这么一句话但信息量很大名字必须是__next__不能是next也不能加别的参数唯一的形参就是self。返回值可以是任意类型Python 不做任何限制。没有下一个值时唯一正确的做法是抛StopIteration。返回None不代表结束它只是你迭代出的一个正常值。另外有个很反直觉的点内置函数next(it, default)的第二个参数并不会传给__next__它只是next()这个内置函数在捕获到StopIteration之后返回的备用值。也就是说__next__永远只负责做一件事要么给值要么抛异常把没有值了该怎么办的善后交给调用方处理。这里建议你把可迭代对象和迭代器两个概念分清楚很多后续的坑都出在这个区分上。表格列一下角色实现了什么能不能被 for能不能被 next()典型例子可迭代对象__iter__可以不行因为它不是迭代器列表、元组、字符串、字典迭代器__iter__和__next__可以可以文件对象、生成器、map对象生成器由生成器函数自动生成可以可以且生成器内部实现了这两个方法(x for x in range(3))列表可以被反复for因为它每次__iter__都返回一个全新的迭代器。一个迭代器对象被for一次之后第二次再for就空了这本身不是 bug而是协议的设计使然。至于这个特性会在实际开发里造成什么坑第三章我会用一个完整案例讲。2. 从零实现一个 Countdown顺序错了会怎样最直接的上手方式是写一个支持倒计时的迭代器。假设需求是从start倒数到1每次产生一个整数数到 0 之后迭代结束。第一次我会写成这样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, end ) # 输出5 4 3 2 1这个类虽然短但__next__里的执行顺序是精心设计的。如果你把内部步骤打乱立刻会出现奇怪的结果。很多人第一次写迭代器时会踩这类顺序坑所以我拆开来说。2.1 为什么必须先保存值、再更新状态、最后 return__next__的核心矛盾是它既要返回当前状态对应的值又要在返回之前把对象推进到下一个状态。如果不保存旧值就贸然更新状态你就再也拿不到当前这个值了。比如这种写法class CountdownBroken: def __init__(self, start): self.current start def __iter__(self): return self def __next__(self): if self.current 0: raise StopIteration self.current - 1 return self.current 1这段代码的输出恰好是5 4 3 2 1但它绕了一个弯先让current变成 4再用self.current 1把旧值算回来。短期看没毛病但可读性很差而且只要再改一次状态逻辑就很容易出错。更经典的错误版本是下面这种def __next__(self): if self.current 0: raise StopIteration return self.current self.current - 1 # 永远执行不到很多人会想当然地在return之后更新状态结果后面那句成了死代码对象永远停在初始值然后外层for就会无限循环输出同一个数。正确的顺序应该是判断是否还有下一个值。如果已经到边界立刻抛StopIteration。把当前要返回的值暂存到一个局部变量里。修改对象内部状态让它指向下一个位置。return暂存的值。这个先读、后改、再返回的顺序本质上是状态机里最常见的模式。任何带状态的迭代器比如文件读取、二叉树前序遍历、分页拉取 API 数据内部的__next__核心都长这样。2.2iter返回 self 的代价Countdown的__iter__写的是return self这意味着Countdown实例本身就是自己的迭代器。这么做的好处是省事坏处是该对象只能被完整迭代一次。你看c Countdown(3) print(list(c)) # [3, 2, 1] print(list(c)) # []第二次list(c)拿到的还是同一个对象但current已经是 0 了所以直接抛StopIteration。这不算错正确的说法是一个迭代器一旦被消费完它就永久处于结束状态。如果你想每次for都从同一个Countdown重新开始需要把__iter__改成返回新对象而不能return self。这一点我强烈建议你写迭代器之前先想清楚我的这个类代表的是一个一次性资源还是可以反复遍历的集合两种角色的协议写法是不同的。在我自己的项目里往往会把这种一次性迭代器设计成只关心__next__把如何产生新迭代器的工作放到另一个__iter__方法中。比如这样class CountdownCollection: def __init__(self, start): self.start start def __iter__(self): return Countdown(self.start)每次for都会通过CountdownCollection.__iter__创建一个全新的Countdown互相不干扰。这才能做到类似列表那样可以反复遍历的效果。3. 二次迭代为空和 StopIteration 被吞一次完整的排查链路理论说再多不如拿真实出过事的代码复盘。下面这段是我在实际项目里遇到过的场景简化版。需求是做一个传感器历史读数的可迭代对象它从某个传感器模拟器里读取过去 100 帧的数据。我最初的实现长这样class SensorHistory: def __init__(self, sensor, frame_count100): self._sensor sensor self._frame_count frame_count self._current 0 def __iter__(self): return self def __next__(self): if self._current self._frame_count: raise StopIteration frame_id self._current self._current 1 return self._sensor.read(frame_id)单次使用时一切正常for data in SensorHistory(sensor)能输出 100 条。但需求方很快反馈了一个诡异的现象同一段代码第一次for能出数据第二次for就什么都不出了而且不报错。3.1 排查过程先别怀疑 Python 3.12 的优化我当时的第一反应也怀疑过是不是 Python 3.12 对迭代器的内部实现做了什么缓存优化。于是我在__next__里加了一行打印然后再次执行两次for第一次for正常打印了 100 条。第二次for确实也进入了__next__但进入后第一行判断就通过了self._current self._frame_count于是立刻抛了StopIteration。外层循环收到这个异常就正常退出了所以表现为空结果、无报错。看到这里问题已经清楚了SensorHistory实例在第一次被迭代时_current从 0 一路涨到 100。第二次for拿到的还是同一个实例它的_current并没有复位自然直接进入结束状态。也就是说问题根本不是 Python 3.12 的新特性而是我错误地把一个传感器的历史记录集合设计成了一次性迭代器。集合应该可被反复遍历而__iter__返回self的做法剥夺了这个能力。3.2 修复方案把集合和游标拆开正确做法是让SensorHistory只负责实现__iter__每次调用都返回一个全新的内部游标对象。游标对象再实现__iter__和__next__负责真正的迭代状态推进。重构后如下class SensorHistory: def __init__(self, sensor, frame_count100): self._sensor sensor self._frame_count frame_count def __iter__(self): return _SensorCursor(self) class _SensorCursor: def __init__(self, history): self._history history self._current 0 def __iter__(self): return self def __next__(self): if self._current self._history._frame_count: raise StopIteration frame_id self._current self._current 1 return self._history._sensor.read(frame_id)改完之后h SensorHistory(sensor) print(len(list(h))) # 100 print(len(list(h))) # 100因为每次list(h)都会先调用h.__iter__()得到一个全新的_SensorCursor状态从 0 开始。这也是为什么标准库里的列表、元组、集合都能反复遍历——它们和真正的迭代器是两回事。3.3 第二个坑except Exception 把 StopIteration 也吞了和迭代一次就空并称迭代器两大经典坑的是异常吞噬。这次问题出现在一个过滤空行的迭代器里。我想从一个内部行迭代器里持续读数跳过空白行直到取到有效内容class NonEmptyLines: def __init__(self, lines): self._lines iter(lines) def __iter__(self): return self def __next__(self): try: line next(self._lines) while line.strip() : line next(self._lines) return line except Exception: return 这个实现能正常跳过空行但你发现问题了吗当内部self._lines迭代到底时next(self._lines)会抛StopIteration而它被except Exception:捕获了函数返回空字符串。外层for永远收不到StopIteration于是循环会一直进行下去反复输出空字符串像死循环一样。这个 bug 之所以隐蔽是因为表面上看函数确实返回了一个值没有异常循环结构也正常。实际上StopIteration虽然是异常但它不是用来表示出错的而是迭代协议里正常的终止信号。在自定义__next__中任何会触及迭代终点的地方都要让StopIteration原样向上抛不能拿它当普通业务异常吞掉。修复很简单要么把except Exception改成更具体的异常要么单独放行StopIterationdef __next__(self): try: line next(self._lines) while line.strip() : line next(self._lines) return line except StopIteration: raise顺带提一下 Python 3.7 之后生成的 PEP 479 规则在生成器函数内部如果StopIteration异常从yield之间冒出来解释器会把它转成RuntimeError而不是静默终止生成器。这是语言层面帮你挡掉上面这类 bug。但 PEP 479 只管生成器管不了你自己手写的__next__。所以手写迭代器时这一条得靠自觉。4. 300 行日志文件里的迭代器什么时候需要自己写next聊到现在你可能已经产生一个疑问yield生成器不是更方便吗三行代码就能造出迭代器为什么还要手动定义__next__确实大多数场景下生成器更短、更清晰。比如同样读取一个文件的行手动迭代器方案要维护缓冲区、行尾状态、结束标记代码很容易长到 30 行。我用一个按块读取超大日志文件的例子来说明这个对比。假设日志文件可能有几个 GB不能直接read().splitlines()必须按固定大小分块读入内存同时要保证不把一行切成两半。手动实现一个BufferedLineReaderclass BufferedLineReader: def __init__(self, fh, block_size8192): self._fh fh self._block_size block_size self._buffer self._finished False def __iter__(self): return self def __next__(self): while \n not in self._buffer and not self._finished: chunk self._fh.read(self._block_size) if not chunk: self._finished True break self._buffer chunk if \n in self._buffer: line, self._buffer self._buffer.split(\n, 1) return line if self._buffer: line, self._buffer self._buffer, return line raise StopIteration这个类的状态有三个底层文件对象_fh、缓冲字符串_buffer、是否读完的标志_finished。每次__next__调用都可能会触发多次底层读取直到缓冲里凑够一整行。这里如果不实现__next__这样的方法光靠yield的线性逻辑反而会绕。再看生成器版本代码明显更短def read_lines(fh, block_size8192): buffer while True: chunk fh.read(block_size) if not chunk: if buffer: yield buffer return buffer chunk lines buffer.split(\n) buffer lines.pop() yield from lines生成器版本用yield from lines把当前块里完整切出的所有行一口气交出去末尾还正确处理了文件最后一行有没有换行符的问题。这段代码在功能上和BufferedLineReader等价但更容易读、更不容易出错。那么问题来了如果你生成器能写出一样的功能为什么还要花时间手动定义__next__就我的实践经验以下几点是我选择手写迭代器的真实理由这个对象本身需要承载状态。比如它要在迭代过程中记录读了多少行上次读到哪个时间戳累计错误次数同时还要暴露curr_position、reset()这类方法。生成器对象虽然也有内部状态但你很难优雅地把那些业务属性挂上去。类结构比函数更适配现有的代码组织。比如解析器框架要求一个类实现迭代协议并作为其他组件的输入。此时把迭代器内嵌成类的__iter__返回值比在类外面散落几个生成器函数更容易维护。你需要同时控制多个游标。手动实现游标类时每个游标都是独立对象互相之间不存在共享状态问题。而用生成器的话同一个生成器对象只能被消费一次如果要并行推进多个遍历得额外复制生成器麻烦。性能敏感且流程复杂。CPython 3.12 的迭代器优化很激进但如果你自己在__next__内部又写了另一堆 Python 层循环优化效果会打折扣。把核心状态机摊平到__next__里配合局部变量加速反而能让解释器更好地做 inline caching。当然如果只是临时过滤数据、倒序遍历一个列表我肯定首选yield。不需要为了用魔术方法而用魔术方法但涉及上面四类场景手动__next__会从能用变成更合适。5. 越边界玩next无限序列、默认值与长度提示__next__不只是用来写有限序列。它真正的能力边界恰好藏在无限和提前终止里。5.1 无限斐波那契迭代器标准库的itertools里有很多消费无限迭代器的工具但它们的前提是你要先能写出一个不依赖内部计数器到头的迭代器。斐波那契就是这个经典例子class Fibonacci: def __init__(self): self._a 0 self._b 1 def __iter__(self): return self def __next__(self): value self._a self._a, self._b self._b, self._a self._b return value这个类没有结束条件只要你有需求它可以无限往下吐数字。如果你直接list(Fibonacci())程序会一直吃到内存爆炸因为list()会一直调用__next__直到收到StopIteration。所以消费无限迭代器的正确方式是自己设置停止条件最常见的就是itertools.islicefrom itertools import islice print(list(islice(Fibonacci(), 10))) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]islice内部会在取够 10 个元素后主动停止不会傻傻地等StopIteration。这也从反面说明一个迭代器是否终止不一定由它自身决定很多时候取决于调用方何时不再调用next()。5.2 next(it, default)优雅绕过 StopIteration手动调用next()时如果你不希望异常打断流程可以传入默认值value next(it, None)当it为空时这不会抛异常而是返回None。itertools的很多场景里从生成器里取第一个可用值也常这么写first_non_empty next((x for x in data if x.strip()), )这里的next拿到的其实是一个生成器表达式对象它内部同样实现了__next__只是你不需要去关注这个细节。传给next()的第二个默认参数只在内置函数层面生效不会传给被迭代对象的__next__这跟我在开头强调过的是一致的。5.3length_hint给消费方一个预分配提示__next__是迭代器的核心但迭代器协议里其实还有一个冷门方法__length_hint__。它从 PEP 424 进入标准库主要用于让list(it)、tuple(it)这类消费操作在真正迭代之前先估算一下迭代器里还剩多少元素从而决定要不要预分配空间。比如我的迭代器知道剩余总数就可以给它加上这个魔术方法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 def __length_hint__(self): return max(0, self.current)__length_hint__的返回值可以是一个约数不要求精确。CPython 内部会通过PyObject_LengthHint拿这个值来做优化。如果一个迭代器恰好能给出长度list()就不必在一个空列表上反复 append 扩容这在高性能场景下是实打实的收益。无限迭代器不要实现__length_hint__因为它的长度是无限的返回任何具体数值都可能误导消费方。这也是为什么itertools.count这样的对象从不实现这个提示。5.4 让实例支持手动 next 和 for 两种消费方式最后补充一个常见需求一个类既想支持for又偶尔想手动next()它。最直接的做法当然是让__iter__返回self。但如果还要支持反复for就得参考第三章的思路让__iter__每次都返回新的游标对象。一个折中方案是类本身实现__next__支持第一次消费同时__iter__返回self。适合那些代表真实单向资源、但偶尔也希望能手动拨一下的对象比如从串口读字节流或从 Kafka 消费消息。这种资源本质是读一条少一条不可重置所以return self反而准确表达了业务语义。如果你真的需要既能反复 for又能手动 next就得我自己说的拆分离方案把重复遍历能力放在外层对象把指针状态放在独立游标里。这个设计虽然多写一个类但语义最清楚后续维护也少踩坑。我自己写解析器、写流式数据接口时一直持一个原则能用生成器绝不自写__next__因为生成器已经把状态机封装得足够好但当你需要跨调用持有业务状态、需要把迭代过程沉淀成一个可供其他模块引用的类对象时手写__next__几乎是唯一可控的方式。这时候请一定记住两件事——先保存旧值再更新状态还有别顺手把StopIteration给吞掉。这两点记牢至少能避开我踩过的大半坑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询