news 2026/9/8 16:55:07

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析Python __next__:迭代器协议核心与避坑实操

说起来有点惭愧,我见过不少写了好几年 Python 的人,for循环用得飞起,但你问他for x in obj这一步到底发生了什么,他第一反应是“就是遍历嘛”。再追问一句:如果让你手写一个支持for的类,你会先实现哪个魔术方法?很多人会愣住,然后答__iter__

没错,但只答对了一半。

__iter__只是把对象变成"可迭代对象"的入口,真正一个一个把值吐出来、决定什么时候停下来的,是今天这篇的主角——__next__。在 Python 3.12 的魔术方法体系里,第 86 篇轮到它,恰好也是我认为整个迭代器协议里最容易被写错、又最值得琢磨的一个方法。

这篇文章适合两类人:一类是刚开始系统过 Magic Methods、对着官方文档不知道从哪下手的初学者;另一类是已经会写迭代器,但被"迭代一次就空了""第二次 for 不出东西""为什么异常被吞了"这类问题坑过的老手。我会把原理、完整代码、踩坑排查都放在一起,争取你看完能直接拿去用。

1. for 循环背地那个 while:next() 到底在找谁的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就会无限循环输出同一个数。

正确的顺序应该是:

  1. 判断是否还有下一个值。如果已经到边界,立刻抛StopIteration
  2. 把当前要返回的值暂存到一个局部变量里。
  3. 修改对象内部状态,让它指向下一个位置。
  4. 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_count=100): 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_count=100): 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改成更具体的异常,要么单独放行StopIteration

def __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(),必须按固定大小分块读入内存,同时要保证不把一行切成两半。手动实现一个BufferedLineReader

class BufferedLineReader: def __init__(self, fh, block_size=8192): 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_size=8192): 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_positionreset()这类方法。生成器对象虽然也有内部状态,但你很难优雅地把那些业务属性挂上去。
  • 类结构比函数更适配现有的代码组织。比如解析器框架要求一个类实现迭代协议,并作为其他组件的输入。此时把迭代器内嵌成类的__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.islice

from 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为空时,这不会抛异常,而是返回Noneitertools的很多场景里,从生成器里取第一个可用值也常这么写:

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给吞掉。这两点记牢,至少能避开我踩过的大半坑。

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

【信息科学与工程学】【产品体系】第二十七篇 存储系统系列一 01

存储系统是一个横跨单机引擎 → 分布式系统 → 硬件指令集 → 云上多地域的四层技术栈。下面按你给的格式,用一组相互关联的表把"各类特性组合"一次性铺开。版本号以各厂商当前 GA 版本为准,功能清单为生产级能力摘要。 一、存储系统总表(编号 厂商+内核+引擎+版…

作者头像 李华
网站建设 2026/9/8 16:54:55

【Day 16】把公司制度文档喂给Agent,它能替HR回答问题

摘要 员工反复问年假、报销&#xff0c;HR成人肉FAQ。本文用RAGFunction Calling多轮记忆搭文档问答&#xff1a;文档自动分块入ChromaDB&#xff0c;Agent自主决定何时检索&#xff0c;回答标来源&#xff0c;追问懂上下文。 先看一段对话&#xff1a; &#x1f464; 年假有…

作者头像 李华
网站建设 2026/9/8 16:51:09

uniapp 微信小程序请求规范

思路是首先是main.js需要将请求接口全部放在vuex里面// 初始API配置initApi(store);export const initApi (store) > {for (let i 0; i < API.length; i) {let item API[i];// 用item.name名作为作为调用方法/*return response Promise*/// 设置请求前缀名item.url $…

作者头像 李华
网站建设 2026/9/8 16:50:26

GitNexus架构解析:如何用工程化机制让AI改代码不失控

开头&#xff1a;当AI把“改一行代码”变成“炸掉整个服务” 如果你最近几年一直在用AI写代码、改代码&#xff0c;大概率碰过这种场景&#xff1a;让AI修一个很小的bug&#xff0c;它非常自信地给你改了一行配置&#xff0c;你觉得没问题就合了&#xff0c;结果线上服务直接起…

作者头像 李华
网站建设 2026/9/8 16:49:41

一串“9”为何能炸穿系统?边界值测试与输入校验实战解析

先问你一个问题&#xff1a;如果你的用户注册接口收到一个字段叫 phone&#xff0c;值是一串99999999999999&#xff0c;你的第一反应是什么&#xff1f;拉黑&#xff1f;直接报错&#xff1f;还是默默存库&#xff1f;我之所以想写这篇东西&#xff0c;是因为这串看起来像键盘…

作者头像 李华