Python可迭代对象与迭代器区别及迭代器协议实战解析 Python 可迭代对象和迭代器的区别看似基础却经常在实际代码里引发问题。能放进 for 循环的对象很多但不是每个这样的对象都叫迭代器。列表、字典、字符串都能遍历却无法直接用于next()生成器和map返回值才是典型的迭代器。要理解 Iterable 与 Iterator不能只背定义还需要把迭代器协议、iter()与next()的执行过程、自定义迭代器的状态管理结合起来看。很多初学者在写自定义类时不知道__iter__应该返回什么在封装分页查询时不知道游标状态放在哪个类里在读取大文件时不清楚为什么readlines()会把内存打满。这些问题本质上是同一个问题没有分清“谁负责产生迭代器”和“谁负责维护遍历位置”。下面从 Python 数据模型的角度逐层拆解给出可以运行的最小案例和一套可复用的排查思路。1. Python 里“能遍历”只是第一步关键在于“能不能产生迭代器”1.1 从 for 循环现象出发列表、字典、文件都能遍历先看一组最基础的现象。在 Python 中以下对象都可以直接放进 for 循环numbers [1, 2, 3] text abc mapping {a: 1, b: 2} with open(sample.txt, r, encodingutf-8) as f: for line in f: ...列表、字符串、字典、文件对象都能遍历。但它们内部的工作方式并不相同。列表一次性把所有元素放在内存中字典遍历时得到的是 key文件对象按行读取时会维护一个读写位置。可以把“能遍历”理解为它们都暴露了某种遍历能力但“如何遍历、能否重复遍历、是否是惰性的”完全不同。真正决定能否被 for 循环使用的规则不是对象是不是 list也不是对象是不是 tuple而是这个对象是否实现了迭代协议或者是否能按照迭代逻辑返回一个迭代器。这个判断标准就是先理解可迭代对象和迭代器的入口。1.2 官方定义中的可迭代对象__iter__或__getitem__在 Python 官方术语中可迭代对象Iterable是能够一次返回一个成员的对象。更严格地说一个对象实现了__iter__()方法或者实现了__getitem__()方法且其下标从 0 开始连续递增那么它就是可迭代对象。用代码判断时可以调用内置函数iter()numbers [1, 2, 3] iterator iter(numbers) print(iterator) # list_iterator object at 0x...能成功调用iter()说明numbers是一个可迭代对象。调用iter()的作用是让这个对象“交出”一个迭代器后续遍历由迭代器完成。这里要特别注意可迭代对象是一个对象迭代器是另一个对象。列表这个可迭代对象本身并不记录“当前遍历到第几个元素”真正记录位置的是它返回的list_iterator。这个区分在自定义类中尤其重要因为一个类可以同时具备可迭代对象和迭代器的角色但最好理解成两个独立职责。1.3 可迭代对象不一定包含所有数据也不一定可以重复遍历可迭代对象并不等价于“容器”。容器通常指的是list、set、tuple、dict这种能够一次保存全部元素的对象。可迭代对象的外延更宽只要能被iter()转换就可以算作可迭代对象。例如文件对象是可迭代的但它并不是一个装满了行的容器。for line in f不会先把整个文件读进内存而是逐行从磁盘读取。再比如range(10**9)也是可迭代对象但如果你调用list(range(10**9))就会因为元素过多而占用大量内存。range不是通过把 10 亿个整数都存放在内存里实现遍历的它只保存start、stop、step三个参数遍历时按公式生成下一个值。所以在设计接口时不要把“可迭代对象”和“序列”混为一谈。函数返回一个可迭代对象不等于把所有数据都准备好它可能是一次性数据流也可能是惰性序列。调用方应该按照迭代器语义消费它不能假设能重复遍历。2. 迭代器协议__iter__返回自身__next__逐次取值2.1 迭代器协议的两个方法迭代器Iterator是指实现了迭代器协议的对象。迭代器协议包含两个方法__iter__()返回迭代器对象自身。__next__()返回序列中的下一个值如果已经没有更多值抛出StopIteration异常。为什么迭代器还需要实现__iter__()因为 for 循环拿到一个对象后会先调用iter()拿到迭代器然后再不断调用next()。如果迭代器本身不实现__iter__()迭代器就无法被直接用于 for 循环。让迭代器的__iter__()返回自身是为了让“迭代器也是可迭代对象”这个关系成立。手工调用的方式如下numbers [1, 2, 3] it iter(numbers) print(next(it)) # 1 print(next(it)) # 2 print(next(it)) # 3 print(next(it)) # StopIteration前三次next()都取到了正常元素第四次抛出StopIteration。StopIteration不是错误信号而是 for 循环的结束信号。for 循环内部会捕获这个异常并停止循环。2.2 for 循环等价写法iter()next()StopIterationfor 循环是一种语法糖它的执行流程可以用while展开。以列表为例numbers [1, 2, 3] it iter(numbers) while True: try: value next(it) print(value) except StopIteration: break这个展开过程解释了三个关键点for 循环不直接对列表取元素而是先调用iter()得到迭代器。每次循环调用next()迭代器内部状态会往后移动。遇到StopIteration时退出循环这个异常被 for 循环自动处理。理解了这一点就能解释很多错误现象。比如next([1, 2, 3])会报TypeError: list object is not an iterator因为列表不是迭代器next()只接受迭代器不能直接作用于可迭代对象。2.3 为什么迭代器是“一次性”的迭代器内部保存了当前遍历位置。每调用一次next()位置就往后移动。位置只前进不后退所以同一个迭代器遍历完之后再次遍历会直接得到空结果。numbers [1, 2, 3] it iter(numbers) print(list(it)) # [1, 2, 3] print(list(it)) # []第二个list(it)返回空列表是因为迭代器已经耗尽。这个特性在生产中非常常见比如读取网络分页数据时如果第一次遍历中断不能用同一个迭代器重新开始必须重新创建迭代器。这里的“状态”是迭代器的关键。列表对象本身没有游标概念列表迭代器才有__next__的推进逻辑。一个对象要成为迭代器就必须承担状态维护职责。3. Iterable 和 Iterator 的真实关系类型判断一测便知3.1 使用 collections.abc 判断如果不想靠文档记忆可以使用collections.abc里的抽象基类来做运行时判断from collections.abc import Iterable, Iterator print(isinstance([1, 2, 3], Iterable)) # True print(isinstance([1, 2, 3], Iterator)) # False print(isinstance(iter([1, 2, 3]), Iterable)) # True print(isinstance(iter([1, 2, 3]), Iterator)) # True从这个结果可以得出一个简洁结论列表是可迭代对象但不是迭代器列表的迭代器既是可迭代对象也是迭代器。生成器对象和map、filter、zip返回的对象也都是迭代器from collections.abc import Iterator print(isinstance((x for x in range(3)), Iterator)) # True print(isinstance(zip([1, 2], [3, 4]), Iterator)) # True print(isinstance(map(str, [1, 2]), Iterator)) # True如果项目里使用类型注解可以结合typing.Iterable和typing.Iterator约束参数类型from typing import Iterable, Iterator def process_items(items: Iterable[int]) - None: for item in items: ... def count_items(it: Iterator[int]) - int: return sum(1 for _ in it)需要注意collections.abc是运行时判断的依据typing中的版本用于类型提示。两者概念一致但使用场景不同。3.2 为什么迭代器一定可迭代可迭代对象不一定是迭代器从类型关系上看Iterator是Iterable的子类。一个迭代器必须实现__iter__()所以它一定可以被 for 循环使用也就是一定可迭代。可迭代对象则不要求实现__next__()。比如列表、元组、字典、集合、字符串它们只实现了返回迭代器的能力自身不维护遍历位置因此不是迭代器。这个关系可以用一句话记住可迭代对象负责提供一个迭代器迭代器负责逐个取值。如果一个对象要直接作为迭代器使用它必须同时实现两种职责。3.3 常见坑把所有可遍历对象当成迭代器导致next()报错实际开发中最常见的错误之一是看到next()函数就把它用在列表或元组上。numbers [1, 2, 3] numbers iter(numbers) # 先转成迭代器 print(next(numbers)) # 1更隐蔽的错误是自定义对象的__iter__()写成了返回列表而不是返回迭代器。class BadIterable: def __iter__(self): return [1, 2, 3] # 错__iter__()必须返回一个迭代器也就是带有__next__()方法的对象。直接返回列表会导致 for 循环执行时报错TypeError: iter() returned non-iterator of type list。在代码审查中一旦看到return self之外的__iter__返回值就要确认它是否真的实现了__next__。如果不是就应该用iter(...)包装。4. 自己实现一个迭代器从最小案例理解协议细节4.1 目标做一个按步长取值的迭代器先实现一个最简迭代器用来验证协议。目标是模拟一个支持自定义步长的数字序列class RangeByStep: def __init__(self, start, stop, step1): self.current start self.stop stop self.step step def __iter__(self): return self def __next__(self): if self.step 0: raise ValueError(step 不能为 0) if self.current self.stop: raise StopIteration value self.current self.current self.step return value使用方法r RangeByStep(0, 10, 2) print(next(r)) # 0 print(next(r)) # 2 print(list(r)) # [4, 6, 8]这里的关键点是__iter__返回self。RangeByStep对象本身就是迭代器因此第一次调用iter(r)时返回的就是同一个对象。后续 for 循环会基于这个对象不断调用__next__。真实项目中如果一个类要支持多次遍历就必须把“可迭代对象”和“迭代器”分成两个类。可迭代对象的__iter__()每次返回一个新的迭代器迭代器负责记录遍历状态。class RangeByStepReusable: def __init__(self, start, stop, step1): self.start start self.stop stop self.step step def __iter__(self): return RangeByStepIterator(self.start, self.stop, self.step) class RangeByStepIterator: 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此时同一个RangeByStepReusable对象可以被多次遍历r RangeByStepReusable(0, 10, 2) print(list(r)) # [0, 2, 4, 6, 8] print(list(r)) # [0, 2, 4, 6, 8]每次iter(r)都会生成一个新的RangeByStepIterator所以遍历状态不会互相污染。这也是 Python 内置容器对象的设计方式列表本身没有遍历位置每次遍历都会创建新的列表迭代器。4.2__iter__必须返回迭代器对象不能返回任意可迭代对象再强调一次__iter__()的返回值语义。协议规定它必须返回迭代器不是返回列表、元组或其他可迭代容器。因为 for 循环拿到返回值后会继续对这个返回值调用next()只有实现了__next__()的对象才能完成这个动作。如果确实想复用已有逻辑可以用iter()把容器包装成迭代器class Numbers: def __iter__(self): return iter([1, 2, 3])这样返回值是列表迭代器满足协议。4.3__next__要正确处理边界并抛出 StopIteration__next__()必须有一个终止条件。到达末尾后要抛出StopIteration而不是返回None或返回某个特殊值。def __next__(self): if self.current self.stop: raise StopIteration current self.current self.current self.step return current如果忘记抛StopIterationfor 循环会一直取到None或其他默认值导致逻辑异常或死循环。实际排查自定义迭代器时第一步就是检查__next__的终止分支。4.4 用__getitem__实现的旧式迭代协议除了__iter__Python 还允许通过__getitem__让对象变得可迭代。iter()在发现对象没有__iter__时会尝试按下标 0、1、2……依次调用__getitem__直到抛出IndexError。class ReadByIndex: def __init__(self, items): self.items items def __getitem__(self, index): return self.items[index]这种写法在兼容旧接口时有用但现在更推荐显式实现__iter__。__getitem__的语义更偏向索引访问__iter__的语义更明确。常见坑是误以为只要返回一个可迭代对象就够了或者把StopIteration和IndexError混在一起。自定义迭代器时两者都要明确处理。5. 生成器是更简洁的迭代器写法但边界行为要清楚5.1 生成器函数通过 yield 实现状态暂停生成器函数是普通函数但函数体里包含yield关键字。调用生成器函数时函数体不会立即执行而是返回一个生成器对象。生成器对象是迭代器。def count_up_to(limit): n 0 while n limit: yield n n 1运行逻辑gen count_up_to(3) print(next(gen)) # 0 print(next(gen)) # 1 print(next(gen)) # 2 # 下一次调用会 StopIteration每次遇到yield函数暂停返回值再次调用next()时从暂停位置继续执行。这个机制省去了手动维护current状态和StopIteration的麻烦因此生成器是实现迭代器的高效方式。生成器还支持yield from用于在一个生成器中委托另一个可迭代对象def chain_two(first, second): yield from first yield from second for x in chain_two([1, 2], [3, 4]): print(x)yield from适合把多个数据源串联成一条流在文件合并、分页拼接等场景很实用。5.2 生成器表达式一行式生成器生成器表达式在语法上很像列表推导式但使用圆括号squares (x * x for x in range(10)) print(squares) # generator object ... print(sum(squares)) # 285 print(list(squares)) # []列表推导式[x * x for x in range(10)]会一次性生成完整列表生成器表达式是惰性计算占用内存更少但不能重复遍历。5.3 生成器是一次性的第二次 for 循环拿到空值生成器对象只能遍历一次squares (x * x for x in range(5)) print(list(squares)) # [0, 1, 4, 9, 16] print(list(squares)) # []这个行为和列表完全不同。如果业务逻辑需要对同一份数据做多轮遍历要么在第一次遍历时转成列表缓存要么重新创建生成器。在写接口封装时要注意文档里明确说明“返回的是生成器只能消费一次”。5.4 无限生成器需要搭配 takewhile 或 islice生成器可以表示无限序列def natural_numbers(): n 0 while True: yield n n 1直接list(natural_numbers())会导致内存耗尽。正确方式是搭配itertools.islice或itertools.takewhile限制消费数量from itertools import islice def natural_numbers(): n 0 while True: yield n n 1 first_ten list(islice(natural_numbers(), 10)) print(first_ten) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]实际项目中轮询、心跳、数据生成器都可能出现类似场景。只要涉及无限序列就必须有显式的退出条件或消费上限。6. 生产场景里的迭代器文件、分页、流式处理6.1 大文件逐行读取不要用 readlines()文件对象本身是可迭代对象逐行 for 循环时使用的是惰性读取with open(access.log, r, encodingutf-8) as f: for line in f: process(line)如果改成for line in f.readlines()readlines()会一次性把所有行加载到内存。当文件有数 GB 时进程内存会快速上涨甚至 OOM。这里并不需要手动实现迭代器Python 的文件对象已经内置了迭代逻辑。重点在于理解“惰性读取”的价值每次只读一行内存占用和文件行数无关。在生产环境处理日志、导出文件、清洗数据时建议先用小文件验证处理逻辑再切到大文件。如果过程中出现内存上涨优先检查是否有人调用了readlines()或者把生成器转换成了列表。6.2 把数据库分页封装为迭代器分页查询是典型的迭代器场景。每页数据相当于一次next()页数增加时从数据库查出下一批数据。把分页逻辑封装成迭代器可以让上层代码通过 for 循环消费数据不需要关心页码。下面是一个简化示例用列表模拟数据库分页结果class PageIterator: def __init__(self, page_size, fetch_page): self.page_size page_size self.fetch_page fetch_page self.page 0 self.index 0 self.current_page_data [] self.exhausted False def __iter__(self): return self def __next__(self): if self.index len(self.current_page_data): if self.exhausted: raise StopIteration self.current_page_data self.fetch_page(self.page) self.page 1 self.index 0 if not self.current_page_data: self.exhausted True raise StopIteration item self.current_page_data[self.index] self.index 1 return item使用示例def fetch_page(page): if page 3: return [] return [frow-{page}-{i} for i in range(2)] for row in PageIterator(2, fetch_page): print(row)这个示例体现了迭代器状态管理的核心index记录当前页内位置page记录当前页号每次数据耗尽时自动加载下一页。真实项目还需要考虑数据库连接、超时重试、异常传递和资源释放但状态切换思路一致。如果底层用的是 SQLAlchemy、Django ORM 或 requests可以把fetch_page换成真实查询函数。外层调用方式不变迭代器屏蔽了分页细节。6.3 封装迭代器时的资源与异常处理在自定义迭代器涉及文件、数据库连接、网络请求时要特别注意退出条件。__next__()抛出StopIteration后外部 for 循环会结束但迭代器内部打开的资源不一定会自动关闭。可靠做法是用上下文管理器with或让迭代器实现close()方法。如果底层请求失败不建议直接吞掉异常。应该把异常传递出去由上游决定是重试还是终止。自定义迭代器的单次next()应该做到“要么返回一个值要么抛出 StopIteration要么抛出真实异常”不要返回 None 来假装成功否则调用方会很难排查空数据问题。开发环境可以通过打印迭代器的类型和当前状态来调试测试环境可以用小数据集构造边界用例例如空页、单页数据、最后一页恰好满页、翻页过程中请求失败这四种情况。生产环境还要在迭代器外层增加日志记录消费了多少条数据、用了多长时间。7. 排查链路遇到list object is not an iterator怎么查7.1 现象 1直接对列表使用 next()错误信息TypeError: list object is not an iterator原因next()只能用于迭代器列表是可迭代对象但不是迭代器。检查方式from collections.abc import Iterator print(isinstance([1, 2, 3], Iterator)) # False处理方式先用iter()得到迭代器。it iter([1, 2, 3]) print(next(it))7.2 现象 2自定义类进入 for 循环时报错错误信息TypeError: iter() returned non-iterator of type list原因__iter__()返回值是列表不是迭代器。检查方式查看__iter__()中return的对象类型如果是列表、元组等容器需要用iter(...)包装。处理方式def __iter__(self): return iter([1, 2, 3])7.3 现象 3同一批数据第二次遍历为空现象第一次 for 循环正常第二次没有任何输出。原因对象是生成器或迭代器第一次遍历已经耗尽。检查方式from collections.abc import Iterator print(isinstance(data, Iterator))处理方式如果是迭代器遍历前转为列表或者每次重新创建生成器如果是自定义可迭代容器让__iter__每次返回新迭代器。7.4 现象 4自定义迭代器没有输出或死循环原因__next__()没有正确维护状态或者终止条件永远不满足也可能是到达末尾后没有抛出StopIteration。检查方式打印current和边界值单步调用next()观察状态变化。处理方式确认__next__()中更新状态和抛StopIteration的分支。把这些场景整理成一张排查表问题现象常见原因检查方式处理建议next()报错 list is not an iterator把可迭代对象当成迭代器isinstance(..., Iterator)先调用iter()for 循环报 non-iterator__iter__返回了列表等容器查看return类型使用iter(...)包装第二次遍历为空使用了生成器或一次性迭代器检查对象是否 Iterator转列表或重建迭代器自定义迭代器无输出__next__状态未更新单步调用next()看状态修正状态更新和终止分支内存占用异常高误用readlines()或列表推导看数据量和峰值内存使用惰性读取或生成器这套链路可以覆盖大部分迭代相关问题。排查顺序建议是先看对象类型再看iter()是否成功然后单步next()验证行为最后看遍历次数和资源释放。8. 速查表、最佳实践清单与学习路径8.1 可迭代对象、迭代器、生成器速查表维度可迭代对象 Iterable迭代器 Iterator生成器 Generator核心方法__iter__或__getitem____next__和__iter__由yield自动生成能否直接next()不能能能能否重复遍历通常可以每次创建新迭代器不可以状态向前移动不可以状态向前移动是否惰性不一定是是典型示例列表、字典、集合、字符串iter([1, 2, 3])生成器函数、生成器表达式内存特点可能一次性存放全部元素逐个产出逐个产出常用来处理无限序列8.2 使用迭代器时的代码审查清单在实际项目里可以用这套清单快速检查迭代相关代码自定义类需要被 for 循环遍历时确认是否实现了__iter__返回值是否为迭代器。如果类需要多次遍历确认__iter__每次返回新的迭代器而不是永远返回self。自定义__next__时确认到达末尾会抛出StopIteration不是返回None。使用next()前确认对象是迭代器不确定时用isinstance(..., Iterator)判断。方法返回生成器时在文档或注释中说明只能消费一次。读取大文件时优先使用文件对象逐行 for 循环避免readlines()。涉及数据库分页、API 游标时把分页状态封装在迭代器内部。遇到无限生成器时使用itertools.islice或其他显式退出条件。自定义迭代器持有文件或连接时明确资源关闭方式。这套清单对初学者很有用也能作为代码评审时的检查项。8.3 扩展方向itertools、协程、异步迭代器理解可迭代对象和迭代器后下一步是学习itertools标准库。itertools.chain、itertools.groupby、itertools.islice、itertools.takewhile可以让迭代器的组合更简洁。再往后生成器与yield from也是理解协程的基础。很多异步编程库在底层同样依赖生成器的暂停和恢复机制。如果要处理流式数据项目可以继续了解异步迭代器__aiter__和__anext__以及async for的语义。回到本文最核心的判断可迭代对象负责提供迭代器迭代器负责维护状态并逐个产出值。只要能清楚区分这两个角色for 循环、生成器、自定义迭代器和分页封装的很多问题都能迎刃而解。平时写代码时可以刻意观察iter()和next()的调用位置把“能遍历”和“是迭代器”这两个概念分开记忆这是理解 Python 迭代机制最有效的一步。