迭代是 Python 最底层的统一协议:可迭代对象实现 iter,迭代器实现 next,for 循环只是这对协议的语法包装。理解协议本身,生成器与惰性工具的行为全部自洽。
可迭代对象与迭代器之分
两个角色必须分清:
- Iterable:实现
__iter__并返回迭代器——「可被遍历的东西」:list、str、dict、set、文件、生成器…… - Iterator:实现
__next__产出下一个值、__iter__返回自身——「正在遍历的游标」
nums = [1, 2, 3]
it = iter(nums) # nums.__iter__() → 拿到游标
next(it) # 1
next(it) # 2
next(it) # 3
next(it) # StopIteration——耗尽信号迭代器自身也可迭代(__iter__ 返回 self),但可迭代对象不是迭代器:list 每次 __iter__ 都新建游标,因此可以反复遍历。
for 循环的本质
it = iter(xs) # 1. 向可迭代对象要游标
while True:
try:
x = next(it) # 2. 拉取下一个值
except StopIteration:
break # 3. 耗尽信号 → 静默退出循环
body(x) # 循环体推论一:for 不依赖下标与序列,任何 Iterable 都能遍历。推论二:只实现 __getitem__ 的旧式对象也能被迭代(解释器从 0 逐个索引直到 IndexError),但协议路径优先走 __iter__;__getitem__ 与协议分发的关系见 特殊方法。
耗尽性:一次性游标
迭代器抛出 StopIteration 后终身耗尽,再 next 只会继续抛:
it = iter([1, 2])
list(it) # [1, 2]
list(it) # []——空,不是再来一遍高发事故:同一迭代器交给两段消费逻辑,第二段默默拿到空集。生成器(生成器)是最常见的迭代器形态,同样一次性——需要多次遍历就重新创建,或先物化成 list。
生成器:迭代器的简便实现
函数体内出现 yield,调用即返回 generator 对象——不执行任何函数体代码,且自动实现完整迭代器协议:
def countdown(n):
while n > 0:
yield n # 每次 next 执行到这里暂停、交出值
n -= 1 # 下次从暂停处继续
list(countdown(3)) # [3, 2, 1]
(x * x for x in nums) # 生成器表达式:一次性管道的字面量写法手写 __iter__/__next__ 的类适合复杂内部状态;简单产出序列,生成器函数与表达式几乎总是更优表达。
惰性求值
迭代器按需产出、从不预物化整个序列:
- 内存 O(1):处理超大数据流不因集合本身爆内存
- 可表达无限序列:
itertools.count()、逐行读文件 - 管道化:多级 map/filter 组合不产生中间 list,中途 break 即短路
代价是单遍、单向、无随机访问。组合子工具箱见 itertools。
内建惰性工具
Python 3 的 zip、enumerate、map、filter 全部返回迭代器而非 list:
z = zip("abc", [1, 2, 3])
z # <zip object at 0x...>
list(z) # [('a', 1), ('b', 2), ('c', 3)]
sum(map(len, words)) # 直接喂给消费者
(a for a in xs if a > 0) # filter 的生成器表达式等价物,更常用细则:
zip按最短输入截断;zip(*xs, strict=True)(3.10+)长度不一致时报错enumerate(xs, start=1)带下标遍历,比手动维护计数器干净- 这些工具相互组合依旧惰性,消费端一处物化即可
API 设计守则:参数收 Iterable、返回 Iterator——上游传 list、tuple、生成器都兼容,下游要物化自己 list()。协议的扩展形态遍布标准库:asyncio 的 async for 是它的异步翻版,itertools 是它的组合子宝库。