Python 基础体系 · 第 15/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。
Python 可迭代对象与迭代器:协议、惰性、耗尽和组合
Python 中的 for 循环、推导式、生成器表达式、map()、zip()、itertools 以及文件逐行读取,都建立在同一组机制上:可迭代对象(iterable)提供数据来源,迭代器(iterator)维护遍历状态,__next__() 一次产生一个值,StopIteration 表示数据流结束。
如果只把“可迭代对象”理解成“可以放进 for 循环的对象”,就很容易忽略几个决定程序行为的重要事实:
- 可迭代对象不一定是迭代器;
- 迭代器通常是有状态、单向、一次性的;
- 惰性并不等于不消耗,也不等于低成本;
- 一个迭代器被某个消费者读取后,其他消费者看到的内容会发生变化;
- 组合迭代器时,输入是否被预先物化、是否共享状态,直接决定内存占用和结果语义。
本文以 Python 3.14 的同步迭代协议为主线,逐步说明这些机制如何连接起来,并在最后讨论生成器和 itertools 中最容易被忽略的边界。
一、先区分四个概念:序列、容器、可迭代对象和迭代器
1. 序列不是可迭代对象的全部
**序列(sequence)**是具有顺序、通常可以通过非负整数索引访问的一类对象,例如:
items = ["a", "b", "c"]
print(items[0]) # a
print(len(items)) # 3
列表、元组、字符串、字节串都是典型序列。序列强调的是:
- 元素有顺序;
- 可以通过下标定位;
- 通常可以通过切片取得子序列;
- 通常可以重复遍历。
但是,Python 的可迭代对象远不止序列。例如:
data = {10, 20, 30}
mapping = {"name": "Ada", "age": 36}
file_obj = open("data.txt", encoding="utf-8")
集合、字典、文件对象都可以参与迭代,但它们的访问语义不同:
for value in data:
print(value)
for key in mapping:
print(key)
for line in file_obj:
print(line)
字典默认迭代的是键,而不是键值对;文件对象迭代的是文本行。可迭代性描述的是“能否逐项提供值”,并不要求对象支持索引、切片或长度。
Python 数据模型将序列、集合、映射等容器分别定义,但这些容器通常通过 __iter__() 提供高效迭代能力。(docs.python.org)
2. 可迭代对象是一个协议概念
**可迭代对象(iterable)**是可以被 iter(obj) 转换成迭代器的对象。
形式化地说,对象 x 满足可迭代协议,可以理解为:
其中:
x是数据来源;i是迭代器;- 后续通过
next(i)逐项取得数据。
通常,一个类通过定义:
def __iter__(self):
...
来实现可迭代协议。__iter__() 应该返回一个迭代器,而不是直接返回单个元素。
例如:
class Numbers:
def __init__(self, values):
self.values = values
def __iter__(self):
return iter(self.values)
numbers = Numbers([10, 20, 30])
print(list(numbers))
# [10, 20, 30]
这里的 Numbers 是可迭代对象,但它本身不负责维护当前遍历位置。每次调用 iter(numbers),它都从内部列表创建一个新的列表迭代器。
3. 迭代器是带状态的可迭代对象
**迭代器(iterator)**不仅能被迭代,还能通过 __next__() 产生下一个元素。
它满足两个核心条件:
class Iterator:
def __iter__(self):
return self
def __next__(self):
...
也就是说:
迭代器的 __iter__() 必须返回自身。collections.abc.Iterator 也正是以 __iter__() 和 __next__() 为核心接口。(docs.python.org)
一个最小的计数迭代器如下:
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
counter = CountDown(2)
print(next(counter)) # 2
print(next(counter)) # 1
print(next(counter)) # 0
try:
print(next(counter))
except StopIteration:
print("exhausted")
输出:
2
1
0
exhausted
这里的 current 就是迭代器状态。每调用一次 next(counter),状态就发生一次变化:
调用前 current |
返回值 | 调用后 current |
|---|---|---|
| 2 | 2 | 1 |
| 1 | 1 | 0 |
| 0 | 0 | -1 |
| -1 | StopIteration |
-1 |
注意:StopIteration 不是“异常情况”,而是同步迭代协议中表示“没有下一个元素”的控制信号。
二、iter() 和 next() 如何组成迭代协议
1. iter(obj) 的主要路径
调用:
iterator = iter(obj)
时,Python 首先尝试取得对象的迭代器。对普通自定义类而言,主要路径是调用 obj.__iter__()。
class Words:
def __iter__(self):
return iter(["Python", "iterator"])
words = Words()
iterator = iter(words)
print(iterator)
print(list(iterator))
iter() 的第二种形式接收一个可调用对象和哨兵值:
iter(callable, sentinel)
它会不断调用 callable(),直到返回值等于 sentinel。例如,按固定大小读取文件:
from functools import partial
with open("data.bin", "rb") as file:
chunks = iter(partial(file.read, 4096), b"")
for chunk in chunks:
process(chunk)
这个组合的逻辑等价于:
while True:
chunk = file.read(4096)
if chunk == b"":
break
process(chunk)
iter(callable, sentinel) 适合把“反复调用直到终止值”的 API 转换成标准迭代器。其停止条件是值相等,不是对象身份,因此哨兵值应当选择不会与正常结果混淆的值。Python 3.14 内置函数文档列出了 iter(iterable) 和 iter(callable, sentinel) 两种调用形式。(docs.python.org)
2. next(iterator) 的语义
调用:
value = next(iterator)
相当于请求迭代器执行:
value = iterator.__next__()
如果还有数据,返回下一个元素;如果已经耗尽,抛出 StopIteration。也可以提供默认值:
iterator = iter([])
print(next(iterator, "empty"))
# empty
此时:
next(iterator, default)
在迭代器耗尽时返回 default,不会抛出 StopIteration。(docs.python.org)
手动消费迭代器的完整写法是:
iterator = iter([1, 2, 3])
while True:
try:
item = next(iterator)
except StopIteration:
break
print(item)
输出:
1
2
3
for 循环只是把这套流程封装起来。
3. for 循环的展开模型
下面的代码:
for item in source:
handle(item)
else:
finish()
可以近似理解为:
iterator = iter(source)
while True:
try:
item = next(iterator)
except StopIteration:
finish()
break
handle(item)
如果循环体中执行了 break,则不会执行 else 分支:
for item in [1, 2, 3]:
if item == 2:
break
else:
print("completed")
这里不会打印 "completed",因为迭代没有通过正常耗尽结束。Python 语言参考明确规定,for 会先对目标表达式调用 iter(),随后不断取得迭代器提供的元素;迭代器耗尽后结束循环,并在没有 break 时执行 else。(docs.python.org)
可以用下面的类观察 for 的真实调用过程:
class Traced:
def __iter__(self):
print("__iter__()")
return iter([10, 20])
source = Traced()
for value in source:
print("value:", value)
输出:
__iter__()
value: 10
value: 20
__next__() 没有出现在输出中,因为这里返回的是列表迭代器。若自己实现迭代器,就能看到每一次 next() 调用。
三、可迭代对象与迭代器的关键区别
1. 可迭代对象通常可以反复创建迭代器
values = [1, 2, 3]
first = iter(values)
second = iter(values)
print(first is second)
# False
print(list(first))
# [1, 2, 3]
print(list(second))
# [1, 2, 3]
列表是可迭代对象。iter(values) 每次返回一个独立的列表迭代器,因此两个迭代过程互不影响。
2. 迭代器通常只能沿一个方向推进
values = [1, 2, 3]
iterator = iter(values)
print(list(iterator))
# [1, 2, 3]
print(list(iterator))
# []
第一次 list(iterator) 已经把迭代器推进到末尾,第二次只能得到空列表。
生成器也是如此:
def numbers():
yield 1
yield 2
yield 3
generator = numbers()
print(list(generator))
# [1, 2, 3]
print(list(generator))
# []
因此,下面的类型注解表达了不同的使用承诺:
from collections.abc import Iterable, Iterator
def consume_once(source: Iterator[int]) -> int:
return sum(source)
def consume_many_times(source: Iterable[int]) -> int:
return sum(source) + sum(source)
第一个函数明确接受一次性数据流;第二个函数隐含要求 source 可以重复产生独立迭代器。如果把生成器传给第二个函数,第二次求和将得到 0:
print(consume_many_times([1, 2, 3]))
# 12
print(consume_many_times(iter([1, 2, 3])))
# 6
计算过程如下:
- 列表:第一次
sum得到6,第二次重新迭代仍得到6,总和为12; - 列表迭代器:第一次得到
6,迭代器耗尽,第二次得到0,总和为6。
这不是类型注解造成的差异,而是对象的状态语义不同。
3. 自身就是迭代器的对象
很多对象同时是可迭代对象和迭代器:
iterator = iter([1, 2, 3])
print(iter(iterator) is iterator)
# True
而列表不是:
values = [1, 2, 3]
print(iter(values) is values)
# False
可用下图概括两者关系:
flowchart LR
A[可迭代对象 Iterable] -->|iter| B[迭代器 Iterator]
B -->|next| C[一个元素]
B -->|next| D[继续推进内部状态]
B -->|耗尽| E[StopIteration]
B -->|iter| B
关键关系是:
Iterable的职责是提供迭代器;Iterator的职责是提供下一个元素;- 一个迭代器本身也必须是可迭代对象;
- 可迭代对象不一定是迭代器。
四、为什么 __getitem__() 也可能让对象可迭代
现代代码通常通过 __iter__() 实现可迭代对象,但 Python 还保留了一个历史兼容路径:如果对象没有 __iter__(),却支持从索引 0 开始的 __getitem__(),Python 可能通过递增索引的方式进行迭代,直到抛出 IndexError。
例如:
class LegacyRange:
def __init__(self, stop):
self.stop = stop
def __getitem__(self, index):
if index < 0 or index >= self.stop:
raise IndexError(index)
return index * 10
values = LegacyRange(3)
print(list(values))
# [0, 10, 20]
这个类没有定义 __iter__(),但仍然可以被 list() 消费。
这解释了一个容易误判的现象:
from collections.abc import Iterable
print(isinstance(LegacyRange(3), Iterable))
# False
print(iter(LegacyRange(3)))
# 可以成功取得迭代器
collections.abc.Iterable 的检查主要识别显式定义或注册的 __iter__(),不会识别只通过 __getitem__() 提供迭代能力的类。判断一个对象是否真正可迭代,可靠方法是直接调用 iter(obj)。(docs.python.org)
这也是为什么下面的代码比 isinstance(x, Iterable) 更可靠:
def is_iterable(value):
try:
iter(value)
except TypeError:
return False
else:
return True
但还要注意:iter(value) 成功,只能证明对象符合同步迭代入口;它不代表迭代一定不会在中途抛出其他业务异常。
五、惰性:值是在什么时候计算的
1. 惰性不是一种数据类型
**惰性(lazy evaluation)**在这里指的是:构造数据处理对象时不立即计算全部结果,而是在消费者请求下一个元素时,才推进上游并计算当前结果。
比较列表推导式和生成器表达式:
numbers = range(5)
list_result = [x * x for x in numbers]
generator_result = (x * x for x in numbers)
print(list_result)
# [0, 1, 4, 9, 16]
print(generator_result)
# <generator object ...>
列表推导式在创建时就完成全部计算并保存结果;生成器表达式创建的是生成器迭代器,表达式会在后续消费时逐项求值。生成器表达式在运行时产生生成器迭代器,而不是立即产生完整列表。(docs.python.org)
可以通过带副作用的函数观察差异:
def transform(value):
print("transform:", value)
return value * 10
eager = [transform(x) for x in range(3)]
print("after eager")
lazy = (transform(x) for x in range(3))
print("after lazy")
print(next(lazy))
print(next(lazy))
输出顺序为:
transform: 0
transform: 1
transform: 2
after eager
after lazy
transform: 0
0
transform: 1
10
生成器表达式创建后,transform() 尚未执行;第一次 next(lazy) 才处理第一个输入。
2. 惰性处理形成数据流
考虑这个管道:
source = range(1, 10)
filtered = (x for x in source if x % 2 == 0)
mapped = (x * 100 for x in filtered)
print(next(mapped))
# 200
为了产生 200,实际发生了:
- 从
source读取1,过滤掉; - 读取
2,过滤通过; - 计算
2 * 100; - 向调用者返回
200。
没有必要先生成所有偶数,也没有必要先生成所有乘法结果。数据按需从右向左触发、从左向右流动:
flowchart LR
S["range(1, 10)"] --> F[过滤偶数]
F --> M[乘以 100]
M --> C["next(mapped)"]
C --> O[返回 200]
如果只请求一个元素,上游通常也只需要推进到能够产生这一个元素的位置。
3. 惰性并不意味着零内存
下面的链条通常只保存少量中间状态:
result = (
value * value
for value in range(10_000_000)
if value % 2 == 0
)
但某些迭代器为了工作,必须缓存输入:
from itertools import product
result = product(range(10_000), range(10_000))
product() 在开始产生结果前,会完全消费输入迭代对象并保存各个输入池,因此输入必须是有限的;它的输出虽然是惰性的,但输入池并不是零内存。(docs.python.org)
因此应区分:
- 输出惰性:结果元组不是一次性全部生成;
- 输入物化:为了生成输出,输入可能先被完整保存;
- 中间缓存:某些组合器可能保留尚未被其他分支消费的数据。
惰性只回答“何时产生结果”,不自动回答“需要多少内存”。
六、耗尽:迭代器是一个状态机
1. 正常状态转换
一个有限迭代器至少可以抽象成两个状态:
ACTIVE --__next__()--> ACTIVE
ACTIVE --__next__()--> EXHAUSTED
EXHAUSTED --__next__()--> EXHAUSTED
更具体地说:
class OneShot:
def __init__(self):
self.done = False
def __iter__(self):
return self
def __next__(self):
if self.done:
raise StopIteration
self.done = True
return "only once"
使用:
iterator = OneShot()
print(next(iterator))
# only once
try:
next(iterator)
except StopIteration:
print("finished")
try:
next(iterator)
except StopIteration:
print("still finished")
输出:
only once
finished
still finished
在协议层面,迭代器耗尽后应继续报告耗尽,而不应重新产生数据。
2. StopIteration.value 与普通 for 的差异
迭代器可以抛出带值的 StopIteration:
class WithReturn:
def __iter__(self):
return self
def __next__(self):
raise StopIteration("final result")
直接捕获时可以读取这个值:
iterator = WithReturn()
try:
next(iterator)
except StopIteration as exc:
print(exc.value)
# final result
但普通 for 循环会把 StopIteration 当作结束信号,不会把 .value 传给循环体。生成器的 return value 则可以通过委托机制被外层生成器获取,这属于 yield from 的控制流语义,后文会说明。
3. 消费者会改变数据源状态
下面的函数名为 peek,但它并不能真正“查看而不消耗”:
def peek(iterator):
return next(iterator, None)
source = iter([10, 20, 30])
print(peek(source)) # 10
print(list(source)) # [20, 30]
peek() 已经消费了第一个元素。对于迭代器而言,“读取”通常就意味着“推进状态”。
如果需要保留已经读出的元素,可以显式缓存:
source = iter([10, 20, 30])
first = next(source, None)
remaining = list(source)
print(first)
print(remaining)
或者使用 itertools.tee() 创建多个逻辑分支,但这并不是免费的复制,后文会讨论其缓存代价。
七、如何实现正确的自定义迭代器
1. 直接实现迭代器
下面实现一个闭区间整数迭代器:
class IntRange:
def __init__(self, start, stop, step=1):
if step == 0:
raise ValueError("step must not be zero")
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if self.step > 0:
if self.current >= self.stop:
raise StopIteration
else:
if self.current <= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
测试:
print(list(IntRange(0, 5)))
# [0, 1, 2, 3, 4]
print(list(IntRange(5, 0, -2)))
# [5, 3, 1]
这里采用半开区间 [start, stop),与内置 range() 的习惯一致。实现时最容易出错的地方是:
step == 0时无限循环;- 正步长与负步长的终止条件相反;
- 抛出
StopIteration后不能重新返回元素; __iter__()如果返回新对象,就不再是典型的自迭代器结构。
2. 可迭代对象与迭代器分离通常更灵活
直接让容器对象自身维护位置,会导致容器只能被安全地遍历一次。更好的结构是:
class IntRangeIterable:
def __init__(self, start, stop, step=1):
self.start = start
self.stop = stop
self.step = step
def __iter__(self):
current = self.start
while (
current < self.stop
if self.step > 0
else current > self.stop
):
yield current
current += self.step
使用:
values = IntRangeIterable(0, 3)
print(list(values))
# [0, 1, 2]
print(list(values))
# [0, 1, 2]
每次调用 iter(values) 都会取得一个新的生成器,因此两次遍历彼此独立。
可以把两种设计对比如下:
| 设计 | __iter__() 返回 |
能否自然重复遍历 |
|---|---|---|
| 容器型可迭代对象 | 新迭代器 | 通常可以 |
| 迭代器对象 | self |
通常不可以 |
| 生成器函数返回的生成器 | 生成器自身 | 通常不可以 |
八、生成器:用 yield 实现迭代器
1. 生成器函数调用时不会立即执行函数体
含有 yield 的函数称为生成器函数。调用生成器函数时,得到的是生成器迭代器;函数体从第一次调用 next() 时才开始执行。执行到 yield 时暂停,并保留局部变量、指令位置和异常处理状态。(docs.python.org)
def events():
print("start")
yield "A"
print("middle")
yield "B"
print("end")
generator = events()
print("created")
print(next(generator))
print(next(generator))
try:
next(generator)
except StopIteration:
print("done")
输出:
created
start
A
middle
B
end
done
时间顺序说明:
events()调用只创建生成器对象;- 第一次
next()执行到第一个yield; - 第二次
next()从上次暂停位置继续; - 第三次
next()执行到函数末尾并抛出StopIteration。
2. yield 是暂停点,不是普通返回
普通 return 结束整个函数;yield 只把一个值交给调用者,然后保留执行现场。
def counter():
value = 0
while value < 3:
received = yield value
print("received:", received)
value += 1
第一次启动时:
generator = counter()
print(next(generator))
# 0
此时生成器暂停在:
received = yield value
因为 next(generator) 是继续执行的方式,当前 yield 表达式恢复后的值为 None:
print(next(generator))
# received: None
# 1
3. send() 将值送回暂停点
send(value) 会恢复生成器,并让暂停处的 yield 表达式求值为 value:
def accumulator():
total = 0
while True:
value = yield total
if value is None:
return
total += value
generator = accumulator()
print(next(generator))
# 0
print(generator.send(10))
# 10
print(generator.send(5))
# 15
执行过程:
| 操作 | 暂停处 yield 的结果 |
total |
产生的下一个值 |
|---|---|---|---|
next() 启动 |
None |
0 | 0 |
send(10) |
10 | 10 | 10 |
send(5) |
5 | 15 | 15 |
生成器尚未启动时,第一次 send() 必须传入 None:
generator = accumulator()
generator.send(10)
会失败,因为生成器还没有暂停在任何一个 yield 表达式上。Python 文档规定,启动生成器时只能使用 send(None),通常直接调用 next() 更清晰。(docs.python.org)
4. throw() 把异常注入生成器
throw() 会在生成器暂停的 yield 位置抛出异常:
def resilient():
while True:
try:
value = yield "waiting"
except ValueError as exc:
print("handled:", exc)
else:
print("received:", value)
generator = resilient()
print(next(generator))
# waiting
print(generator.throw(ValueError("bad input")))
# handled: bad input
# waiting
如果生成器内部没有捕获这个异常,异常会传播给调用者:
def simple():
yield 1
generator = simple()
print(next(generator))
try:
generator.throw(RuntimeError("failure"))
except RuntimeError as exc:
print(type(exc).__name__, exc)
throw() 的重要意义在于:消费者可以把外部故障传递给数据生产逻辑,而不是只能通过共享变量通知生成器。
5. close() 请求生成器清理并结束
close() 会在暂停位置注入 GeneratorExit:
def resource_stream():
print("open resource")
try:
yield 1
yield 2
finally:
print("close resource")
generator = resource_stream()
print(next(generator))
# open resource
# 1
generator.close()
# close resource
生成器中的 finally 会执行,因此可以用于清理资源。若生成器在收到 GeneratorExit 后仍然产生值,Python 会抛出 RuntimeError。生成器方法 send()、throw() 和 close() 的行为由生成器协议定义;Python 3.13 起,如果生成器在关闭时通过 return 返回值,该值会由 close() 返回。(docs.python.org)
不过,不能把垃圾回收当作资源管理策略。尤其是文件、网络连接、数据库游标等外部资源,应通过 with 或显式关闭保证生命周期。Python 数据模型也明确建议显式释放外部资源,而不要依赖对象何时被垃圾回收。(docs.python.org)
九、yield from:委托迭代与返回值传递
1. 直接转发子迭代器的值
下面是一个简单的委托:
def child():
yield "child-1"
yield "child-2"
def parent():
yield "parent-1"
yield from child()
yield "parent-2"
print(list(parent()))
# ['parent-1', 'child-1', 'child-2', 'parent-2']
yield from child() 不只是一个简化版循环,它还会把外部对父生成器的操作转发给子迭代器:
send()可以转发给子生成器;throw()可以转发给子生成器;close()会参与子生成器的关闭;- 子生成器结束时的
StopIteration.value会成为yield from表达式的结果。
2. 用 return 从子生成器返回结果
def child():
yield 1
yield 2
return 99
def parent():
result = yield from child()
print("child result:", result)
yield 3
print(list(parent()))
输出:
child result: 99
[1, 2, 3]
这里的执行过程是:
parent()委托给child();child()产生1和2;child()执行return 99;- 子生成器结束,内部表现为
StopIteration(99); yield from child()表达式的值为99;parent()打印结果后继续产生3。
语言参考规定,yield from <expr> 的表达式必须是可迭代对象;子迭代器结束时,StopIteration.value 成为 yield from 表达式的结果。(docs.python.org)
十、组合:把多个数据流连接起来
1. map()、过滤和生成器表达式构成流水线
source = range(1, 11)
pipeline = (
value * value
for value in source
if value % 2 == 0
)
print(list(pipeline))
# [4, 16, 36, 64, 100]
这个流水线可以拆成:
先保留满足:
的值,再计算:
因此输入 2, 4, 6, 8, 10 被转换为 4, 16, 36, 64, 100。
如果只取前两个结果:
from itertools import islice
source = range(1, 10_000_000)
pipeline = (
value * value
for value in source
if value % 2 == 0
)
print(list(islice(pipeline, 2)))
# [4, 16]
islice() 会从输入迭代器中选取指定位置的元素,不支持负索引;当输入是迭代器时,跳过和读取都会推进原迭代器。(docs.python.org)
2. chain() 连接多个输入
from itertools import chain
result = chain([1, 2], (3, 4), "56")
print(list(result))
# [1, 2, 3, 4, '5', '6']
chain() 逐个耗尽输入对象:
第一个输入:[1, 2]
第二个输入:(3, 4)
第三个输入:"56"
它不会把所有输入先合并成一个新列表,因此适合顺序连接多个数据源。但如果某个输入本身是一次性迭代器,chain() 消费它之后,该输入就已经耗尽。
3. zip() 是同步推进
left = iter([1, 2, 3])
right = iter(["a", "b"])
print(list(zip(left, right)))
# [(1, 'a'), (2, 'b')]
print(list(left))
# [3]
print(list(right))
# []
每次产生一个元组,zip() 同时从各个输入迭代器读取一个值。默认情况下,最短输入耗尽后停止,因此第三个元素 3 没有配对结果。
当长度不一致属于错误时,可以使用严格模式:
left = [1, 2, 3]
right = ["a", "b"]
try:
print(list(zip(left, right, strict=True)))
except ValueError as exc:
print(exc)
严格模式会把输入长度不一致报告为 ValueError,比静默截断更适合数据对齐场景。
4. zip() 的分组技巧依赖“同一个迭代器”
itertools 文档中的分组模式是:
from itertools import zip_longest
def grouper(iterable, size, fillvalue=None):
iterators = [iter(iterable)] * size
return zip_longest(*iterators, fillvalue=fillvalue)
测试:
print(list(grouper("ABCDEFG", 3)))
# [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', None, None)]
关键不是创建了三个独立迭代器,而是:
[iter(iterable)] * size
复制的是同一个迭代器引用。每一轮 zip_longest() 会连续从同一个底层迭代器取 size 个元素,因此形成固定大小分组。
如果误写成:
[iter(iterable) for _ in range(size)]
对于列表等可重复迭代对象,将得到多个独立迭代器,结果会变成同一批元素被重复读取,而不是连续分组。
十一、itertools 的组合语义与内存边界
itertools 的核心价值不是“把循环写短”,而是提供一组遵循迭代协议的组合器。它们的行为必须从三个问题判断:
- 是否惰性产生输出;
- 是否完全消费输入;
- 是否共享或缓存输入状态。
1. groupby() 是连续分组,不是全局聚合
from itertools import groupby
data = [
("error", 1),
("error", 2),
("info", 3),
("error", 4),
]
for key, group in groupby(data, key=lambda item: item[0]):
print(key, list(group))
输出:
error [('error', 1), ('error', 2)]
info [('info', 3)]
error [('error', 4)]
groupby() 只把相邻且键相同的元素放到同一组。因此它与 SQL 的 GROUP BY 不同:
输入:error, error, info, error
输出:error组, info组, error组
如果希望所有相同键进入同一组,必须先按相同键排序:
data = sorted(data, key=lambda item: item[0])
for key, group in groupby(data, key=lambda item: item[0]):
print(key, list(group))
groupby() 返回的每个分组本身也是迭代器,并且与外层 groupby() 共享底层输入。外层向前推进后,之前的分组通常就不能再读取,因此需要保留时必须立即物化为列表。(docs.python.org)
错误示例:
for key, group in groupby("AAABBC"):
groups = list(group)
# 这里如果继续推进外层,group 已经没有可读内容
正确的保留方式:
saved = []
for key, group in groupby("AAABBC"):
saved.append((key, list(group)))
print(saved)
# [('A', ['A', 'A', 'A']), ('B', ['B', 'B']), ('C', ['C'])]
2. takewhile() 会消费第一个失败元素
from itertools import takewhile
source = iter([1, 2, 6, 3, 4])
prefix = takewhile(lambda x: x < 5, source)
print(list(prefix))
# [1, 2]
print(list(source))
# [3, 4]
值 6 不满足条件,因此没有出现在 prefix 中;但它已经被从底层输入读取出来并丢弃。takewhile() 的语义是“持续读取直到第一次失败”,而不是“找到一个边界但保留边界元素”。官方文档明确指出,第一个不满足条件的元素已经被消费,无法通过原输入迭代器再次访问。(docs.python.org)
这在解析协议流、日志流或分隔记录时尤其重要。若边界元素也有意义,就不能直接假设 takewhile() 会把它留在输入中。
3. tee() 提供分支,但需要缓存领先数据
from itertools import tee
source = iter([1, 2, 3])
first, second = tee(source)
print(next(first))
# 1
print(next(first))
# 2
print(list(second))
# [1, 2, 3]
为了让 second 仍然能够读取 1 和 2,tee() 必须缓存 first 已经消费、而 second 尚未消费的元素。
因此,tee() 的内存成本取决于分支之间的消费速度差异:
first 读取很快
second 读取很慢
=> 缓存不断增长
如果两个分支几乎同步消费,缓存通常较小;如果一个分支远远领先,缓存可能接近领先分支已经读取的数据量。官方文档将 tee() 描述为从一个可迭代对象返回多个独立迭代器,并给出了基于共享链表的近似实现。(docs.python.org)
tee() 不是“复制任意迭代器的完整快照”,它更接近“为多个读取游标维护共享历史”。
4. product() 输出惰性,但输入要先保存
from itertools import product
pairs = product([1, 2], ["a", "b"])
print(next(pairs))
# (1, 'a')
print(list(pairs))
# [(1, 'b'), (2, 'a'), (2, 'b')]
输出逐个产生,但输入池会在开始输出前被保存。对于有限小输入,这通常很方便;对于大型输入,输入物化本身可能成为内存瓶颈;对于无限输入,product() 无法正常开始,因为它需要先完成输入消费。官方文档明确说明,product() 会完全消费输入,并且只适用于有限输入。(docs.python.org)
5. 无限迭代器必须配合边界
from itertools import count, islice
infinite = count(10, 2)
print(list(islice(infinite, 5)))
# [10, 12, 14, 16, 18]
count() 可以产生无限序列;但下面的代码不会结束:
list(count())
即使生成器和 itertools 都是惰性的,list() 仍然要求把输入全部消费并保存,因此对无限输入会无限运行并持续占用内存。
安全组合通常需要至少一个终止条件:
from itertools import count, takewhile
values = takewhile(lambda x: x < 10, count(0, 2))
print(list(values))
# [0, 2, 4, 6, 8]
终止条件必须能够在有限步骤内成立;如果谓词永远为真,takewhile() 仍然不会结束。
十二、常见误解与失败表现
1. 误以为所有可迭代对象都能调用 len()
values = (x for x in range(3))
len(values)
结果:
TypeError: object of type 'generator' has no len()
可迭代性和可计数性是两个不同协议:
- 可迭代:能否通过
iter()逐项读取; - 有大小:是否实现
__len__(); - 容器:通常同时支持成员测试、迭代和长度,但不是所有可迭代对象都是容器。
对流式数据,要求 len() 往往意味着必须先消费或物化数据,这与惰性模型冲突。
2. 误以为迭代器可以重新开始
iterator = iter([1, 2, 3])
print(next(iterator)) # 1
print(list(iterator)) # [2, 3]
print(list(iterator)) # []
要重新开始,应保留可迭代对象,而不是只保留迭代器:
values = [1, 2, 3]
for _ in range(2):
print(list(values))
如果源数据来自网络、文件或数据库游标,则通常不能简单重播,需要显式缓存、重新打开源或设计可重放日志。
3. 误以为 list() 只是查看内容
iterator = iter([1, 2, 3])
snapshot = list(iterator)
print(snapshot)
# [1, 2, 3]
print(list(iterator))
# []
list(iterator) 是一个消费者,它会把迭代器推进到耗尽。调试时直接打印 list(iterator) 可能改变后续业务逻辑。
4. 误以为生成器函数调用就执行了代码
def load():
print("loading")
yield 1
generator = load()
print("created")
输出只有:
created
"loading" 要等到:
next(generator)
才会出现。若生产代码依赖函数调用立即完成校验、打开文件或建立连接,使用生成器函数时需要重新检查生命周期假设。
5. 在迭代期间修改容器
values = [1, 2, 3, 4]
for value in values:
if value % 2 == 0:
values.remove(value)
print(values)
结果可能是:
[1, 3]
但这种写法容易跳过元素,因为列表迭代器的位置与列表内容变化相互作用。更清晰的做法是创建新列表:
values = [1, 2, 3, 4]
values = [value for value in values if value % 2 != 0]
print(values)
# [1, 3]
这里不是迭代协议保证“修改一定报错”,而是具体容器和实现对修改期间行为的保证有限。工程代码不应依赖修改与遍历同时进行时的偶然结果。
十三、诊断迭代问题的方法
1. 先确认对象类型和身份
def inspect_iterable(value):
print("type:", type(value))
print("is iterator:", iter(value) is value)
例如:
inspect_iterable([1, 2, 3])
# type: <class 'list'>
# is iterator: False
inspect_iterable(iter([1, 2, 3]))
# type: <class 'list_iterator'>
# is iterator: True
iter(value) is value 是判断一个对象是否表现为自身迭代器的直接实验,但它不能替代对协议语义的完整理解。
2. 用带日志的迭代器定位提前消费
class LoggedIterator:
def __init__(self, iterable):
self._iterator = iter(iterable)
def __iter__(self):
return self
def __next__(self):
value = next(self._iterator)
print("consume:", value)
return value
测试:
source = LoggedIterator([1, 2, 3])
pipeline = (x * 10 for x in source if x > 1)
print(next(pipeline))
输出:
consume: 1
consume: 2
20
为了生成第一个结果 20,流水线消费了两个输入元素。这个方法可以发现:
- 某个过滤器是否提前读取了边界元素;
- 某个组合器是否比预期多消费;
- 多个消费者是否共享同一个底层迭代器;
- 是否在调试代码中意外调用了
list()或tuple()。
3. 区分“未执行”和“已耗尽”
生成器有两个容易混淆的状态:
未启动:函数体尚未执行
已耗尽:函数体已经结束,不能再产生值
示例:
def source():
print("start")
yield 1
generator = source()
print("created")
print(next(generator))
print(list(generator))
print(list(generator))
输出:
created
start
1
[]
[]
第一次 list(generator) 为空,可能有两种完全不同的原因:
- 生成器本来就没有更多元素;
- 生成器之前已经被其他代码消费过。
因此诊断时要追踪“谁先调用了 next()、list()、sum() 或 for”。
十四、同步迭代与异步迭代不是同一个协议
本文讨论的是同步迭代:
iter(obj)
next(iterator)
StopIteration
异步迭代使用另一组协议:
aiter(obj)
anext(iterator)
StopAsyncIteration
对应的语法是:
async for item in async_source:
...
异步生成器使用 async def 和 yield,产生异步迭代器;其 __anext__() 返回可等待对象,结束时通过 StopAsyncIteration 表示完成。Python 3.14 语言参考将同步生成器与异步生成器分别定义,二者不能仅通过把 for 改成 async for 来互换。(docs.python.org)
这一区分很重要:
for item in async_source:
...
不能替代:
async for item in async_source:
...
因为同步协议不会自动等待异步迭代器返回的可等待对象。
十五、如何选择数据表示:可迭代对象还是迭代器
可以根据调用方需要的语义选择接口。
1. 当数据可以重复读取时,返回可迭代对象
例如:
class UserCollection:
def __init__(self, users):
self._users = tuple(users)
def __iter__(self):
return iter(self._users)
每次遍历都从同一份稳定数据创建新迭代器。适合:
- 内存中的集合;
- 可重复查询的索引;
- 需要多次遍历的配置或元数据;
- 调用方不应影响对象本身状态的 API。
2. 当数据来自一次性来源时,明确返回迭代器
例如:
def read_lines(path):
with open(path, encoding="utf-8") as file:
for line in file:
yield line.rstrip("\n")
这个函数返回生成器,调用方应理解为:
lines = read_lines("data.txt")
for line in lines:
...
而不是:
print(list(lines))
print(list(lines)) # 第二次为空
如果业务需要重复处理,应在边界处明确物化:
lines = list(read_lines("data.txt"))
代价是内存使用增加,但生命周期和重放语义变得清晰。
3. 类型注解应表达消费方式
from collections.abc import Iterable, Iterator
def summarize(values: Iterable[int]) -> int:
return sum(values)
def stream(values: Iterable[int]) -> Iterator[int]:
for value in values:
yield value * 2
Iterable[int] 表示调用方只要求“能够取得迭代器”;Iterator[int] 表示返回的是一次性推进的数据流。若函数会多次遍历参数,文档和类型设计都应反映“可重复遍历”的要求,而不能只写成含糊的 list 或 object。
十六、核心结论
Python 迭代系统可以压缩为一条精确的数据流链:
可迭代对象
--iter()-->
迭代器
--next()-->
一个元素
--重复推进-->
更多元素
--StopIteration-->
耗尽
其中最重要的边界是:
- 可迭代对象是数据来源,迭代器是带状态的读取游标。
- 迭代器的
__iter__()返回自身;可迭代对象的__iter__()通常返回新的迭代器。 for循环本质上反复调用next(),并把StopIteration转换为正常结束。- 惰性意味着按需计算,不意味着没有缓存、不消耗输入或没有资源成本。
- 迭代器通常是一次性的;任何
next()、for、list()、sum()都可能推进它。 groupby()、tee()、product()等组合器有不同的共享、缓存和物化语义,不能只根据“返回迭代器”判断其内存行为。- 生成器在此协议上增加了暂停、恢复、发送值、注入异常、关闭和委托控制流。
掌握这些协议后,列表、文件、生成器、数据库游标、map()、zip() 和 itertools 就不再是彼此孤立的 API,而是同一种“按需推进的数据流”在不同场景下的具体实现。
系列导航与关联阅读
- 系列入口:Python 完整学习路线:从语言模型、并发到 Web、数据、AI 与生产交付
- 上一篇:Python 装饰器:函数包装、参数化、类装饰器与元数据
- 下一篇:Python 生成器:yield、send、throw、close 与委托
- 延伸:Python itertools:惰性组合、无限序列、分组和内存边界
官方资料
本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论