自定义迭代器深度实践:从迭代器协议到生成器的内存优化与状态管理
1. 为什么需要自定义迭代器1.1 一次遍历引发的“性能焦虑”大约半年前我在某数据采集系统里处理日志文件。日志文件不大单份几十MB但架不住数量多每天数千份。最初的实现很简单一次性把所有文件内容读入列表再逐行处理。跑起来倒也能用就是内存曲线很吓人几份文件处理完进程直接吃掉几GB内存。为了这事我一度怀疑是不是哪里产生了内存泄漏。后来排查了一圈发现罪魁祸首就是那个“读入列表”的操作。日志文件解析这种场景本质上是逐行消费数据是流水式的根本没理由全量堆积在内存里。但当时用的第三方SDK已经把结果封装成了列表我改不动。那怎么办只能自己动手重写一个按需输出的数据结构——这就是我设计自定义迭代器的起点。所谓自定义迭代器通俗点说就是你让一个对象支持for循环、支持next()逐个取值并且取值过程完全由你掌控可以边算边给、边读边给、边从网络拉边给。你不需要把整个数据序列准备好再返回而是像拧水龙头一样需要多少出多少。这个需求在真实业务里一点都不冷门。文件逐行读取、数据库游标分批拉取、树形结构的深度优先遍历、实时数据源的无尽推送……凡是“数据很多但一次又用不完”的场景都适合用自定义迭代器来解。它的核心价值就两条省内存、能表达复杂的遍历顺序。这两条在数据量上来之后不是锦上添花而是雪中送炭。1.2 迭代器协议所有语言里的“君子协定”很多人写了好几年代码每天都在用for i in something但没想过这个something到底要满足什么条件才能被遍历。这里有个核心概念叫迭代器协议。不同语言叫法不同但本质一模一样对象必须实现“给我下一个元素”和“遍历结束了通知我”这两个行为。在Python里这两个行为分别对应__iter__和__next__两个特殊方法。__iter__返回迭代器自身__next__返回下一个元素没元素了抛StopIteration异常。在Java里是Iterator接口的hasNext和next。在C#里是MoveNext和Current。在JavaScript里是[Symbol.iterator]方法配合next()返回{ done, value }。风格各异骨架一致。我当年第一次看Python的迭代器协议时觉得StopIteration这个异常设计很别扭——正常结束为什么要抛异常后来用久了才明白这正是“迭代完成”这个状态在协议层最干净的表达方式。你用for循环时解释器自动帮你把这个异常当成“循环结束”信号根本不需要你关心。理解这一点后面写自定义迭代器时就会少很多困惑。协议本身不复杂但“让你自己的类支持迭代”和“让你自己的迭代器跑得优雅”是两码事。前者只是语法层面的接通后者涉及状态管理、边界条件、性能开销、并发安全甚至语义设计。本文要聊的就是后面这些东西。2. 设计一个迭代器的完整思路2.1 无状态迭代器的快速实现先看最简单的情况你要设计一个迭代器它的遍历序列是纯计算出来的不需要维护额外的上下文状态。比如生成斐波那契数列这个序列无限长显然不能预生成列表只能靠迭代器边算边给。在Python里最直白的方式是定义一个类class FibonacciIterator: def __init__(self, max_count): self.max_count max_count self.count 0 self.a, self.b 0, 1 def __iter__(self): return self def __next__(self): if self.count self.max_count: raise StopIteration result self.a self.a, self.b self.b, self.a self.b self.count 1 return result这段代码里有几个细节值得琢磨。一个是__iter__返回self。为什么不能返回别的因为迭代器协议要求iter(iterator)返回自身这样for循环里才能安全地重复调用。如果你把__iter__写成返回一个新对象大概率会在嵌套遍历或多重迭代时出诡异问题。另一个是状态更新的顺序。result self.a这行必须在更新self.a之前执行。你可以用临时变量也可以利用Python的多重赋值特性。这里用多重赋值更简洁但要特别注意右边的求值顺序——Python会先算出元组(self.b, self.a self.b)然后再解包赋值。所以写成self.a, self.b self.b, self.a self.b是完全安全的不需要中间变量。第三个细节是StopIteration的触发时机。我把判断放在了next方法的最前面这意味着迭代到第max_count次之后下一次调用就会抛异常。这个语义和for循环配合得很顺循环内部在拿到最后一个合法值之后会再调用一次next来得知结束。但这里有个坑如果max_count是0也就是迭代器一开始就应该结束那么第一次调用next就会抛异常。这对for循环没问题但对某些期望“先判断再取值”的调用方式就不够友好。所以后面我在设计有状态迭代器时会刻意检查这种边界情况。2.2 有状态迭代器状态管理的艺术无状态迭代器只是热身。实际业务里的迭代器绝大多数都需要维护“当前进度”这个状态。拿树形结构遍历举例一棵多叉树你想用for node in tree的方式做深度优先遍历这个迭代器必须在每次next调用时记住“我访问到哪个节点了”“子节点栈里还剩下什么”。我实现过一个多叉树的深度优先迭代器核心思路是用一个显式栈来模拟递归调用。每次取出栈顶节点然后把它的子节点逆序压栈这样出栈顺序才符合深度优先的访问顺序。class DepthFirstIterator: def __init__(self, root): self.stack [root] def __iter__(self): return self def __next__(self): if not self.stack: raise StopIteration node self.stack.pop() # 注意这里逆序压栈 for child in reversed(node.children): self.stack.append(child) return node这个设计最妙的地方在于显式栈把递归调用栈的状态完全拆出来了。你能看到每一步发生了什么也能在某个时刻暂停遍历保存迭代器对象下次继续。这个“可暂停、可恢复”的特性是递归写法很难直接给出的高级能力。有状态迭代器最大的风险是状态损坏。比如你迭代到一半外部改了数据结构栈里的数据就可能和现实对不上。所以当你设计一个针对可变集合的迭代器时一定要考虑遍历期间集合被修改了怎么办是抛出异常还是容忍不一致Java里ArrayList的迭代器会维护一个modCount计数检测到并发修改就抛ConcurrentModificationException。这种“fail-fast”机制虽然粗暴但能把问题在最早期暴露出来而不是让程序带着幽灵数据继续跑。我在设计这类迭代器时通常会在__iter__里返回一个迭代器对象但让这个对象持有集合的快照引用并记录初始状态版本号。每次next时校验版本号不一致就抛异常。这种案头工作看起来繁琐但能省下后面排查“遍历结果不对”的巨大成本。2.3 生成器比手写类更省事的实现路径如果你觉得写一个完整类、维护状态、处理异常太繁琐Python里有一条捷径生成器函数。只要函数体里有yield关键字这个函数就变成了生成器函数调用它返回的不是普通值而是一个生成器对象这个对象本身就是迭代器。拿前面的斐波那契例子用生成器重写def fibonacci(max_count): a, b 0, 1 count 0 while count max_count: yield a a, b b, a b count 1简洁到令人发指。你不需要定义__iter__、__next__不需要手动抛StopIteration控制流到yield时自动暂停下次next时从暂停处继续执行。这就是所谓的“协程式迭代”。我早年间有个误区觉得生成器就是语法糖手写类才是正规军。后来深入理解后才发现生成器不仅仅是写法上的简化它在设计层面解决了一个手写类很不好处理的问题暂停点状态的保存。手写类你得把总进度细化成一个个字段生成器则完全免了——局部变量天然就是状态yield时被编译器保存恢复时无缝还原。那是不是有了生成器手写迭代器类就没用了也不是。有些场景下你确实需要更精细的控制比如迭代器的状态需要对外暴露调用方能查询“当前游标位置”迭代器需要支持重新定位 seek迭代器需要实现多个不同的遍历策略而不是固定一个迭代逻辑非常复杂拆成多个辅助方法更清晰这种情况下显式的类结构反而更合适。我的习惯是如果迭代逻辑能用二三十行生成器搞定就用生成器如果需要对迭代行为做额外操作、或者逻辑复杂度超出了“流式输出”的范畴才用类。3. 核心实战三种典型的自定义迭代器设计3.1 分页拉取型迭代器这类迭代器数据量极大无法一次性获取只能按页从远端读取。我手头有个典型案例某业务系统的接口一次最多返回500条明细但总数有几十万条。如果写一堆for page in range(total_page)的样板代码会让业务逻辑里充斥着分页细节。这时候设计一个分页拉取的迭代器业务里只需要for item in fetcher就可以顺序消费全部明细。以下是按这个思路实现的Python例子通用性比较强你可以直接抄作业class PaginatedFetcher: def __init__(self, fetch_page, page_size500): self.fetch_page fetch_page self.page_size page_size self.page 0 self.current_page_data [] self.index_in_page 0 self.exhausted False def __iter__(self): return self def __next__(self): if self.index_in_page len(self.current_page_data): if self.exhausted: raise StopIteration # 拉取下一页 self.page 1 self.current_page_data self.fetch_page(self.page, self.page_size) self.index_in_page 0 if not self.current_page_data: self.exhausted True raise StopIteration item self.current_page_data[self.index_in_page] self.index_in_page 1 return item这里的精妙之处在于状态机的设计。current_page_data是当前页的缓冲区index_in_page是缓冲区内的光标位置。每次next先看缓冲区还有没有数据没有就触发下一页拉取拉取结果为空说明全部拉完了于是把exhausted置位。下次再调用时因为exhausted已经为真直接抛StopIteration。这里有个细节很多人容易写错空页判断。有些接口在“没有更多数据”时返回的不一定是空列表而是返回一页但长度不足page_size。如果你只依赖“空列表”作为结束信号就可能漏掉最后一页的尾巴。稳妥的做法是当返回的数据长度小于page_size时视为最后一页。但要注意如果你的fetch_page无法保证这一点比如接口损坏时恶作剧地重复返回500条不重样数据迭代器就会进入死循环。所以更保守的兜底是同时加上最大拉取页数限制。另外这个迭代器的边界条件我设计成“index在进入时就检查”。初始状态下current_page_data是空列表index_in_page是0所以第一次调用next就会触发拉页。这个初始化方式不需要额外标记“还没拉过第一页”天然地用空缓冲区代表“需要拉新页”逻辑干净。3.2 树形结构遍历型迭代器树的遍历方式是面试高频题但绝大多数讨论只停留在“用递归打印节点”。在工程里把树的遍历封装成一个迭代器收益是巨大的你可以在循环里剪枝、可以中途停止、可以把这个迭代器传给任何接受可迭代对象的通用函数。本质上你是把“遍历策略”从“业务逻辑”里解耦出来。以二叉树的中序遍历为例。中序的顺序是左子树、根、右子树。递归写法非常直观但你要转成迭代器就得思考如何在next之间记住“当前正在往左走”还是“刚回溯回来”。我用一个栈来模拟这个递归过程class InOrderIterator: def __init__(self, root): self.stack [] self._push_left(root) def _push_left(self, node): while node: self.stack.append(node) node node.left def __iter__(self): return self def __next__(self): if not self.stack: raise StopIteration node self.stack.pop() self._push_left(node.right) return node这个迭代器的工作原理是初始化时沿着左子树一路压栈这样栈顶是最左下角的节点它是中序遍历的第一个输出节点。每次弹出栈顶节点后如果它有右子树就沿着这个右子树的左链全部压栈。这样就保证了“左-根-右”的整体顺序。用栈模拟递归这段逻辑我建议你亲手跟一遍。比如一棵只有三个节点的树根节点A左子节点B右子节点C。初始化时栈为[A, B]A先进B后进第一次next弹出BB没有右子树输出B第二次next弹出A把A的右子树C压栈输出A第三次next弹出C输出C。顺序是B、A、C完全符合中序。如果你需要前序、后续、层序思路类似只是入栈时机和遍历策略不同。这类迭代器的通用价值在于树的遍历策略是稳定不变的但遍历时的业务动作千变万化。把遍历封装成迭代器后业务代码只需要关心“拿到一个节点后做什么”而不需要关心“下一个节点是怎么来的”。3.3 惰性计算型迭代器惰性计算是迭代器最迷人的应用场景之一。所谓惰性就是值在“被需要的那一刻”才计算而不是在“创建迭代器的那一刻”算好。这在处理无限序列时特别重要。前面提的斐波那契是典型例子它无限长只有惰性生成才有意义。这里再举一个我实际用过的场景一个业务报表系统需要生成连续自然日期列表范围可能跨好几年。如果用列表把所有日期预生成光字符串对象就有上千个但业务可能只需要每周统计一次。设计一个日期范围迭代器from datetime import datetime, timedelta class DateRangeIterator: def __init__(self, start_date, end_date): self.current start_date self.end_date end_date def __iter__(self): return self def __next__(self): if self.current self.end_date: raise StopIteration result self.current self.current timedelta(days1) return result同类方案还有按周迭代、按月迭代、按工作日迭代等。这种迭代器在数据处理领域能帮上大忙。比如你统计某些连续时间段的数据如果把时间范围当作一个可迭代对象传给统计函数逻辑会变得非常清晰。惰性计算的核心代价是什么是时间换空间。每次取一个值都需要执行一次计算或读取操作通常比直接访问内存列表要慢一些。所以惰性迭代器最适合的场景是“数据量大、单次取值代价可接受”。如果你的数据量小、需要反复随机访问老老实实用列表反而更合适。4. 常见问题与排查技巧4.1 迭代器无法重置“我明明重新调用iter()了为什么迭代器还是空转”这是最常见的坑。原因通常是迭代器类把__iter__写成了return self导致同一对象无法从头开始迭代。如果你写了一个无状态的迭代器类这个设计没有问题但如果你希望同一个对象能被多次独立遍历就必须让__iter__返回一个全新迭代器而不是自身。让我用一个生活类比来解释。书页本身就像迭代器你从头读到尾就不能再从尾读回头除非你翻页回去。而图书馆里的一本书相当于一个可迭代对象每次“借阅”都能从开篇开始。Python里list是可迭代对象它的__iter__返回的是一个独立的新迭代器而我们自己写的很多迭代器是迭代器兼可迭代对象iter(iterator)返回自身。怎么判断你的设计是否合理答案是区分这两个概念。class RangeIterable: def __init__(self, n): self.n n def __iter__(self): return RangeIterator(self.n) class RangeIterator: def __init__(self, n): self.i 0 self.n n def __next__(self): if self.i self.n: raise StopIteration result self.i self.i 1 return result这里RangeIterable是可迭代对象每次for循环都会新建一个RangeIterator所以可以随便重复遍历。这也是标准库的做法。4.2 迭代过程中修改集合引发的错乱另一个高频问题出现在集合类型迭代器上。比如你迭代一个列表在循环体里往列表末尾追加新元素你以为会“无限迭代”结果在某些语言里抛了异常在另一些语言里则可能陷入死循环。Python中对列表的迭代器会跟踪大小一旦发现列表长度变化就立刻抛RuntimeError: dictionary changed size during iteration之类的异常。这个行为是好是坏从安全角度讲是好事它把你的错误暴露在事故发生点。从使用体验上讲很多新手会觉得莫名其妙。正确做法是如果确实需要在迭代过程中补充任务不要直接往被迭代的集合里加而是用一个辅助队列收集新任务等当前迭代完成后再统一处理。tasks [task1, task2] pending [] # 不能直接修改 tasks先收集新任务 for t in tasks: # process t pending.append(task3) tasks.extend(pending)这是工程上非常常见的一个模式叫“迭代期任务收集”几乎任何需要动态扩展任务列表的轮询系统都用得上。4.3 迭代器不释放资源如果你的迭代器持有文件句柄、网络连接、数据库游标迭代完或者提前中断时忘记关闭这些资源就可能泄漏。Python的迭代器协议本身不负责帮你关闭资源你必须自己处理。好在有contextlib.closing和with语句可以配合。不过更优雅的做法是用生成器加try/finallydef read_lines(path): f open(path, r, encodingutf-8) try: for line in f: yield line finally: f.close()这样不管for循环是正常结束还是中途被breakfinally都会关闭文件。如果你担心调用方根本不用for循环而是手动调用next那这个设计也依然安全只要生成器被垃圾回收finally也会执行。4.4 大坑迭代器协议里的“一个元素被吞了”还有一个隐蔽问题值得单独说。某些迭代器在设计时会为了“测试是否还有下一个元素”而提前向前读一个元素。例如你想实现一个带“预览”功能的迭代器需要先获取下一个值判断是否要继续然后再返回当前值。如果实现不当预览那个值就永久丢失了。一个经典解决方案是使用“缓冲一个元素”的内部队列。也就是next时先看看队列里有没有缓存没有才真正去源头取。执行预览时可以把取到的值先放回队列。这个模式叫pushback或peekable迭代器。Python标准库里itertools.tee也能实现类似效果它可以把一个迭代器拆成两个独立迭代器每个都能完整遍历原序列。但要注意tee会缓存未消费的元素如果原迭代器离终点很远而你只消费了一个分支内存消耗可能非常高。5. 性能与效率的深度考量5.1 惰性求值与内存占用自定义迭代器最大的收益就是内存占用大幅下降。处理百万行文本全量加载进列表需要几十到几百MB而一个逐行读取的迭代器同一时刻只有一行在手内存占用可以忽略不计。这点在操作大数据集时尤其重要。我处理过一份两千万行的数据清洗任务如果用列表存储光字符串对象就可能导致内存溢出换成迭代器后程序跑完也就几百MB的峰值内存。但要注意惰性求值不是免费的。每次next都涉及函数调用、状态更新可能比直接遍历一个连续数组慢一个数量级。所以在某些性能敏感的热循环里你需要权衡。一个折中方案是“分块缓冲”迭代器不是一次只产出一个元素而是一次产出一个小批次比如512条。这样既能控制内存又能摊薄逐元素调用的开销。Python标准库的pandas.read_csv的chunksize参数正是这个思路。5.2 迭代器开销的定量分析很多人对“迭代器速度慢”这事耿耿于怀。其实要分场景看。在CPython里生成器和迭代器的next调用是有额外字节码解释开销的。如果你的循环体本身逻辑很重比如做正则匹配、网络请求那迭代器的开销占比可以忽略不计如果循环体只是个累加器那迭代器的开销可能占到大头。我在一个文本解析场景里实测过一个纯手写的迭代器按行解析2万行JSON日志耗时大约120ms换成传统列表遍历同样数据耗时约80ms。差异部分是迭代器多出来的调度开销。但两者在内存上的差异就有意思了列表方式需要保存全部解析结果约消耗350MB迭代器方式只保留当前行峰值30MB。这个对比说明什么说明迭代器和列表之间不是简单的“谁更快”的问题而是“用空间换时间”还是“用时间换空间”的取舍。如果你确信数据量不大、内存充裕列表更简单如果数据量大或不确定迭代器几乎是唯一合理的选择。5.3 单次 next 调用的效率优化如果next调用确实成了瓶颈有几个优化套路值得记录。第一个是减少属性访问次数。每次self.current这种属性查询在Python里比局部变量慢。如果你在热循环里反复访问同一个属性值得把它先赋值给局部变量。但注意如果你的迭代器是有状态的局部变量只在当前next调用内部有效每次调用开始时再加载。第二个是用__slots__减少实例内存占用。如果你的迭代器类会创建海量实例每个实例默认有一个__dict__字典来存属性内存开销不小。定义__slots__可以避免这个字典class FastIterator: __slots__ (current, limit) def __init__(self, limit): self.current 0 self.limit limit第三个是极小众但很有效的优化使用 C 扩展或 JIT 库。如果你在Python里写迭代器发现性能实在兜不住可以改用Cython或者PyPy来跑同一段代码。PyPy的JIT对迭代器代码优化效果显著同一套生成器代码在PyPy上的速度往往比CPython快数倍。6. 多语言视角下的迭代器实现要点6.1 Java 迭代器接口设计Java的迭代器在设计上和Python有细微但重要的差异。Java的Iterator接口有两个核心方法hasNext()用于判断是否有下一个值next()用于取出值。它把“判断”和“取值”拆成了两个独立操作这就解决了Python里“预览”难题——你可以安全地调用hasNext而不用担心消耗元素。实现一个Java版的分页迭代器骨架是这样的class PaginatedIteratorT implements IteratorT { private final ListT currentPage; private int index; private boolean needsFetch true; public boolean hasNext() { if (index currentPage.size()) return true; // 尝试拉取下一页... return nextPage ! null !nextPage.isEmpty(); } public T next() { if (!hasNext()) throw new NoSuchElementException(); return currentPage.get(index); } }Java的关键点是hasNext可能会触发网络请求或IO所以它的性能特征更难以预测。在设计时要把“判断”和“取值”的成本权衡清楚尽量让hasNext轻量不然每次循环判断都拉一次远端数据性能雪崩。6.2 C# 迭代器与 yield 的编译期魔法C# 的迭代器设计是我目前见过的语言里最舒适的之一。你直接用yield return写一个方法编译器自动生成一个状态机类帮你处理MoveNext、Current、Dispose等一系列琐事。C#里的yield return非常有意思的地方是它的状态机会把方法中所有局部变量提升为字段。所以如果你在迭代器方法里用了大量局部变量它们的生命周期会被延长可能造成非预期的内存占用。这在写“长流水线”的迭代器链时尤其需要注意。C# 另一个特性是IAsyncEnumerableT可以在迭代过程中安全地执行异步IO。比如逐行读取远程文件时每读一行都可能需要await而一般迭代器无法异步。IAsyncEnumerable弥补了这个缺陷使迭代器真正适用于现代异步编程场景。6.3 JavaScript 的迭代器与生成器JavaScript 的迭代器协议很轻量[Symbol.iterator]()返回{ next: () ({ value, done }) }。生成器函数function*内部可以用yield行为与Python几乎一致。区别在于JavaScript的next方法可以接收参数也就是说你可以在迭代过程中向生成器内“塞回”一个值。这个特性很多语言没有它让“双向数据流”成为可能。举个例子function* echoGenerator() { const received yield ready; yield received: ${received}; }这个生成器在第一次next()时返回“ready”调用方在generator.next(hello)时会把hello作为上一次yield的结果值传回去。这种双向通信在协程和流式处理里很有价值Python里的send()也有类似能力但JavaScript把这个概念放到了迭代器协议的一等公民位置上。7. 实战项目一个日志解析迭代器的完整演进7.1 需求与第一版实现最后分享一个我实际做过的完整案例一个用于多文件日志合并解析的迭代器。需求很简单给定一组日志文件的路径按“文件顺序行号顺序”合并输出所有行支持可选的正则过滤。面对多文件最自然的方案是把每个文件的打开和读取封装成一个小迭代器然后在一个主迭代器里串联它们。这个“迭代器的组合”正是标准库itertools.chain做的事情。但业务上还需要过滤所以链式处理之前先套了一层过滤迭代器。第一版实现我直接用生成器因为生成器兼容yield from语法串联逻辑非常简洁import re def parse_logs(file_paths, patterns): compiled [re.compile(p) for p in patterns] for path in file_paths: with open(path, r, encodingutf-8) as f: for line in f: if all(p.search(line) for p in compiled): yield line.strip()这段代码只有十几行但功能完整。它有状态吗有但状态被生成器自动管理在文件句柄和行指针里。它开销大吗每行正则匹配是重头戏迭代器本身的开销可以忽略。7.2 需求的演化状态暴露与断点续传后来业务方提出一个新需求日志解析过程能查看到“当前正在处理哪个文件、当前行号”并且在崩溃后能在指定文件行号断点续传。生成器的写法没办法优雅地暴露这些状态。于是我把实现改成了类class LogParser: def __init__(self, file_paths, patterns): self.file_paths file_paths self.patterns [re.compile(p) for p in patterns] self.current_index 0 self.current_line_no 0 def __iter__(self): return self._generate() def _generate(self): for idx, path in enumerate(self.file_paths): self.current_index idx with open(path, r, encodingutf-8) as f: for line_no, line in enumerate(f, 1): self.current_line_no line_no if all(p.search(line) for p in self.patterns): yield line.strip() def restore(self, file_index, line_no): 重置迭代状态到某个文件的某一行之前。 self.current_index file_index self.current_line_no line_no # 具体恢复逻辑需要对文件做截断读取这里略去这个迭代器对象本质上变成“可观测的游标”外部能检查它当前位置也能通过restore重定位。这是生成器原生无法提供的灵活度。这里我踩过一个坑__iter__返回_generate()生成器而不是self。这意味着每次for循环得到的都是一个全新的生成器迭代器的状态共享关系需要额外设计。在这个例子里因为current_index和current_line_no是定义在类实例上的多个生成器会共享同一份状态如果并发遍历就会混在一起。所以我又加了一个锁或者强制规定只能单线程串行使用。这个约束我在注释里写得很清楚。7.3 性能对比与最终选型改造完后我做了个基准测试。同一份5万行日志文件三种方案实现方式耗时内存峰值能否断点续传普通列表全量加载0.8s128MB容易生成器迭代0.7s8MB困难类迭代器0.9s10MB容易列表大法确实不算慢内存是硬伤。生成器迭代内存最省速度也快。类迭代器内存稍高但可以恢复状态。最终我选择了类迭代器版本因为业务更看重断点续传能力性能上0.2s的差距完全可接受。这个取舍过程在真实项目中每天都在发生并不存在“绝对最好的迭代器”只有“当前场景下最合适的迭代器”。8. 写给新手的避坑指南与设计清单8.1 动手前先回答三个问题很多新手拿到自定义迭代器的需求上来就开写结果越写越乱。我建议动手前先过一遍这三个问题。第一个问题你需要的是一次性遍历还是可重复遍历如果是前者迭代器类让__iter__返回self最简单如果是后者就必须把“可迭代对象”和“迭代器”拆成两个类。第二个问题迭代速度重要还是内存重要重要的事情说三遍先看数据量。数据量在几千到几万列表和迭代器无所谓数据量百万级迭代器就是必须项如果数据量只有几十个没必要追求迭代器的高级设计。第三个问题你需要在迭代过程中修改集合吗如果需要就得走“任务收集”模式或者干脆允许快照但快照同样耗内存。8.2 十不为清单我在实际经历中总结了一些反面教训整理成“十不为”清单不要在__next__里修改影响迭代状态的共享变量尤其是在多线程环境不要在__iter__里写耗时的IO操作因为iter()本身应该轻量不要忘记处理空集合和只含一个元素的集合边界测试必须覆盖不要用递归实现深度未知的树的迭代器浅层递归没问题但深层递归会爆栈或超出迭代器自身的能力不要千遍一律地让__iter__返回self先想清楚是否真的需要“一次性”不要在生成器里保存超大的临时集合那等于把迭代器的内存优势还回去不要把迭代器名写成Iterator就完了给迭代器做清晰的生命周期注释调用方需要知道能否重置不要抛裸的StopIteration以外的异常除非你明确知道调用方会处理不要在finally里再次yield那是语法错误而且过早yield会让迭代器提前结束不要以为所有语言里迭代器都“天然线程安全”自定义迭代器的线程安全99%要靠自己8.3 设计清单速查最后给一张设计清单方便你在写完迭代器后自查[ ]__iter__语义明确返回新迭代器还是self[ ]__next__在无元素时正确抛StopIteration[ ] 空集合边界情况有测试[ ] 中间打断比如break不会导致资源泄漏[ ] 迭代过程中如果集合被修改行为有定义报错或容忍[ ] 工作量大时考虑分块批量返回[ ] 可观测性需要时暴露当前游标或状态信息[ ] 文档注明迭代器是否可重置、是否线程安全这个清单我每次写迭代器都会过一遍。你别看它长真正踩过坑的人才知道每条背后都是一个血泪故事。我这些年写自定义迭代器的最大体会是迭代器不是一个“用完就丢”的工具类它其实是“把数据的生产过程和消费过程解耦”的设计模式。真正理解它你会发现自己对代码的结构感都会上一个台阶。数据从哪里来、如何准备、什么时候停止这些问题的答案不再散落在业务逻辑里而是被收拢到一个边界清晰的迭代器协议中。这个“收拢”的动作就是自定义迭代器设计的精髓。