本文目录
一次性 list(range(10**9)) 在内存里不现实,但「每次只拿下一个数」可以。Python 的迭代模型就是:有一个对象按协议不断产出值,消费方用 for 或手动 next() 取,取不到时协议结束。列表、文件行、字典键、网络流,表面语法都是 for x in thing,底层统一走迭代协议——不必事先知道长度,也不必将全部元素物化到内存。这种「按需计算」是生成器、推导式、标准库 itertools 的共同基础;写管道式数据处理时,优先想「能否用迭代器串起来」往往比先 list() 再处理更省内存。
异常篇讲控制流岔路;这一篇讲顺序推进:谁负责「下一个值」,何时停,以及 yield 如何把普通函数变成惰性序列。弄懂协议后,你能解释「为何 iter 过的生成器不能第二次 for」「为何 list 可以同时被两个 for 各遍历一遍」这类常见问题。迭代与下文的生成器是 Python 里「延迟执行」的两根支柱,异步协程在概念上也与生成器有亲缘,但调度模型不同,别混为一谈。
迭代协议:iter 与 next
可迭代对象(iterable)能交给 iter() 得到迭代器(iterator)。迭代器必须实现:
| 方法 | 含义 |
|---|---|
__iter__() | 返回迭代器自身(迭代器里通常 return self) |
__next__() | 返回下一个值;没有更多值时抛 StopIteration |
class Countdown:
def __init__(self, start: int) -> None:
self._n = start
def __iter__(self) -> "Countdown":
return self
def __next__(self) -> int:
if self._n <= 0:
raise StopIteration
value = self._n
self._n -= 1
return value
for n in Countdown(3):
print(n) # 3, 2, 1容器类(如 list)的 __iter__ 返回新的迭代器对象,所以可以同时有多路遍历,各自维护 cursor。迭代器本身一般是一次性的:next(it) 耗尽后只能再 iter() 一个新的——对生成器尤其如此。内置 iter(callable, sentinel) 是另一形态:反复调无参 callable,直到返回值等于 sentinel,适合按块读 socket、扫日志行尾等。
collections.abc.Iterable 与 Iterator 在文档层描述这两层概念:前者「能产出迭代器」,后者「自己就是迭代器」。写类型标注时 Iterable[int] 常指「可以 for 出 int」,比具体 list/tuple 更宽。判断「能不能 for」在概念上看有没有 iter 协议即可。
for 循环在做什么:脱糖
for x in obj: 大致等价于:
_it = iter(obj)
while True:
try:
x = next(_it)
except StopIteration:
break
# 循环体因此 for 不要求 obj 有 __getitem__ 或 __len__,只要 iter(obj) 能工作。历史上旧式序列用非负整数 __getitem__ 下标迭代,现代代码应实现迭代协议。StopIteration 是协议结束信号,不要在业务代码里靠 catch 它当普通分支——在 __next__ 和生成器内部由解释器处理;在 generator 外层手动 next 时,StopIteration 表示「正常结束」,与 ValueError 这类真正的错误不同。
列表推导 [x*2 for x in range(3)] 立刻构造列表;生成器表达式 (x*2 for x in range(3)) 返回生成器,按需算下一个,不占满内存。把推导式外层括号改成圆括号就是 genexp,这是语法层面的懒求值。函数参数里只有一个生成器实参时可省略额外括号:sum(x*x for x in range(10))。
生成器函数与 yield
函数体里出现 yield,调用该函数不会立刻执行到结束,而是返回一个生成器对象——它已经是迭代器,自带 __iter__ / __next__:
def fib_up_to(limit: int):
a, b = 0, 1
while a <= limit:
yield a
a, b = b, a + b
gen = fib_up_to(10)
print(next(gen)) # 0
print(next(gen)) # 1
print(list(gen)) # [1, 1, 2, 3, 5, 8]每次 next(gen) 从上次 yield 处恢复局部变量和执行位置,直到再次 yield 或函数结束。函数正常结束会隐式抛 StopIteration;在 Python 3.3+,return value 会把 value 放进 StopIteration 实例的 .value,供 yield from 消费,日常业务代码很少依赖。
yield from iterable 把产出委托给子迭代器,适合递归遍历树、扁平嵌套,也比手写 for 循环逐 yield 更短:
def walk(node: list | int) -> int:
if isinstance(node, int):
yield node
else:
for child in node:
yield from walk(child)
print(list(walk([1, [2, [3]], 4]))) # [1, 2, 3, 4]生成器是一次性的:跑完就耗尽,要再遍历需重新调用生成器函数得到新对象。把生成器当「可重复播放的列表」是常见误解;需要多次遍历就 list(gen) 物化,或每次重新调用工厂函数。
常见用法与边界
大数据与管道:读 GB 级日志、分页 API,用生成器链式过滤,中间不必落地成 list。状态机:用 yield 暂停在多个阶段;复杂异步则交给 async def(异步章节再讲)。内存边界:sum(x*x for x in range(10**7)) 安全;sum([x*x for x in range(10**7)]) 可能先撑爆内存。
send、throw、close 是生成器作为「半协程」的扩展接口:调用方可以往暂停的生成器里塞值或注入异常。初学先掌握产出侧 yield 即可;框架代码里才会常见 gen.send()。itertools 模块提供大量返回迭代器的工具(chain、islice、groupby),它们都建立在同一协议上,值得按需查阅而非死记。
猜输出练手:
def g():
print("start")
yield 1
print("mid")
yield 2
print("end")
it = g()
print("before first next")
print(next(it))
print("before second next")
print(next(it))输出顺序:before first next → start → 1 → before second next → mid → 2。可见 yield 之前的 print 在第一次 next 才执行——函数在第一次被推进时才「活过来」,此前只创建了生成器对象。再 next(it) 会打印 end 并抛 StopIteration。
别混淆 iterable 与 iterator:list 可多次 iter() 得到新迭代器;生成器对象既是 iterable 也是 iterator,只能走一遍。list(gen) 会耗尽 gen,之后同一对象再无元素。字典在 Python 3 里默认迭代键;要键值对用 .items(),它同样返回 iterable。
enumerate、zip 与 itertools 直觉
enumerate(it, start=0) 返回 (index, value) 的迭代器,内部仍是协议实现,不占额外列表。zip(a, b, strict=True)(3.10+)按最短或 strict 对齐多路迭代;传三个以上 iterable 时一次产出多个元素。它们都「懒」,适合大文件逐行配对。
names = ["alpha", "beta", "gamma"]
for i, name in enumerate(names, start=1):
print(i, name)itertools.count、cycle、repeat 产生无限或长跑迭代器,务必在业务里自己 break,别直接 list()。islice(it, n) 对任意 iterable 取前 n 项而不先算全长。理解这些工具的共同点是:返回迭代器,不 eager 物化。
手动迭代时注意 StopIteration 只在协议层出现。写 wrapper 时若要把迭代器转 list 缓存,用 cached = list(it) 明确消耗一次;别假设 for 完了还能 next 同一 iterator。
生成器与协程语法都含 yield,但协程函数用 async def,产出 awaitable,别与生成器混在同一函数体——解释器在定义期就分流。异步迭代用 async for,对应 __aiter__ / __anext__,留给异步章节。
自定义 iterable 若只需遍历一次,可直接实现 iterator(__iter__ 返回 self);若需多次 for,应像 list 那样在 __iter__ 里返回新迭代器对象,把状态放在独立的小类里。这个设计选择在写框架时经常遇到:外部 API 看起来是「可 foreach 的集合」,内部是游标还是快照,由 __iter__ 实现决定。
字符串也是 iterable,但 for ch in s 产出长度为 1 的 str,不是 Unicode 码点;要码点用 for cp in s.encode() 或第三方工具。range 本身是不可变序列,也是 iterable,不占存储所有整数的空间——CPython 用 start/stop/step 算术生成 next。
列表、元组、集合、字典视图都实现迭代,但 mutating 容器在迭代过程中改大小可能抛 RuntimeError: dictionary changed size during iteration 或 list 的类似错误——迭代前拷贝 list(d) 或迭代键再查值是常见规避。生成器没有这个问题,因为它不暴露底层结构给外部同时修改。
写 __next__ 时别 return None 表示结束,必须 raise StopIteration;return 值只在 3.3+ 生成器 return 语义里特殊。手动驱动迭代时用 next(it, default) 可在结束时返回 default 而不 catch StopIteration,适合「可能没有下一项」的宽松读取。
从可迭代到迭代器的分工,可以这样记:iterable 负责「能开始遍历」,iterator 负责「记住遍历进度」。list 是 iterable 不是 iterator;调用 iter(my_list) 得到 list_iterator,消耗完后 list 本身仍可再次 iter。生成器函数返回的对象同时扮演两者,所以第二次 for 必须重新调用函数。面试与 code review 里常考这一点,答「生成器一次性、list 可重复」即源于此。
推导式与生成器表达式语法相近,选择依据是「要不要立刻占用内存」。中间结果若只消费一次,genexp;若要索引、len、多次遍历,list。tuple() 包 genexp 可得到不可变快照,适合作为 dict 键或 set 元素的去重前处理。
内置 reversed(seq) 要求 seq 已实现 __reversed__ 或支持 __getitem__ 整数索引;普通生成器只能 forward,不能 reversed——若需反向,要么物化 list,要么写双向协议。sorted(iterable) 则必须消耗整个 iterable 才能排序,对大生成器要先想清楚是否承受内存峰值。
迭代协议也是 async for、协程 yield from 的语法基础:同一套「暂停—产出—继续」思想在不同子系统里复用。先把同步 iterator 跑通,再读异步版本会少很多「为什么又停住」的困惑。日常写业务,90% 场景用 for + 生成器或标准库迭代工具即可,不必每个类都手写 __next__。
练习时可以对照 dis.dis 看 for 循环字节码:LOAD_ITER、FOR_ITER 等指令就是协议在虚拟机层的体现。知道「语法糖底下是什么」有助于 debug 生成器挂起、迭代器耗尽后仍被 next 等问题——这类 bug 常在 lazy pipeline 中间某一环悄悄消费掉 iterator。排查时在可疑环节 print(id(it)) 或包一层 logging 迭代器,能很快定位是哪一步把 generator 吃光。团队内约定「谁创建 generator、谁负责消费」也能减少重复 iter 的协作 bug。生成器命名建议体现「单次遍历」语义,例如 iter_rows() 而非 rows(),避免被当成可重复读取的 list。这与 API 设计里的命名 honesty 同一道理:名字应反映可否重复消费。
小结
迭代协议把「序列」抽象成「能反复 next 的对象」;for 是语法糖。__iter__ 负责拿到迭代器,__next__ 负责产出或 StopIteration。yield 用最省事的写法实现该协议,得到生成器。下一篇看另一种成对语义:with 与上下文管理器,保证进入和退出资源对称。