文章目录
-
- [一、从一个看似普通的 for 循环说起](#一、从一个看似普通的 for 循环说起)
- 二、迭代协议的两个层次
-
- [2.1 可迭代对象(Iterable)](#2.1 可迭代对象(Iterable))
- [2.2 迭代器(Iterator)](#2.2 迭代器(Iterator))
- [2.3 两者的本质区别](#2.3 两者的本质区别)
- [三、`for` 循环的完整执行流程](#三、
for循环的完整执行流程) - [四、深入 CPython:`iter()` 做了什么](#四、深入 CPython:
iter()做了什么) - [五、`iter()` 的两参数形式:一个被低估的特性](#五、
iter()的两参数形式:一个被低估的特性) - 六、自定义迭代器的工程实践
-
- [6.1 可迭代对象与迭代器分离的设计价值](#6.1 可迭代对象与迭代器分离的设计价值)
- [6.2 实战案例:数据库结果集分页迭代器](#6.2 实战案例:数据库结果集分页迭代器)
- 七、内置迭代器的行为细节
-
- [7.1 列表迭代器是一次性的](#7.1 列表迭代器是一次性的)
- [7.2 字典的迭代顺序](#7.2 字典的迭代顺序)
- [7.3 文件对象是迭代器](#7.3 文件对象是迭代器)
- 八、协议检查与类型注解
-
- [8.1 运行时协议检查](#8.1 运行时协议检查)
- [8.2 类型注解中的迭代相关类型](#8.2 类型注解中的迭代相关类型)
- 九、迭代器协议的全景图
- 十、常见陷阱与最佳实践
-
- 陷阱一:把迭代器当可迭代对象共享
- [陷阱二:`zip` 和 `map` 等惰性对象只能遍历一次](#陷阱二:
zip和map等惰性对象只能遍历一次) - [陷阱三:`StopIteration` 在生成器内部被吞掉(Python 3.7+)](#陷阱三:
StopIteration在生成器内部被吞掉(Python 3.7+)) - 最佳实践小结
- 十一、与前序文章的联系
- 小结
一、从一个看似普通的 for 循环说起
这段代码每个 Python 程序员都写过:
python
names = ["Alice", "Bob", "Carol"]
for name in names:
print(name)
输出是三行名字,没有异议。问题是:Python 在执行 for name in names 这一行时,具体做了什么?
大多数教程到这里只会说"for 循环会遍历可迭代对象"------这句话没错,但它把真正有趣的部分藏起来了。
把同样的问题换一种问法:能不能在不使用 for 语句的前提下,得到完全相同的效果?
python
names = ["Alice", "Bob", "Carol"]
# 手动模拟 for 循环的行为
it = iter(names) # 第一步
while True:
try:
name = next(it) # 第二步
print(name)
except StopIteration: # 第三步
break
这两段代码的行为完全等价------for 语句就是上面这个 while 循环的语法糖。
这里面出现了三个关键要素:iter()、next() 和 StopIteration。理解了这三件事,迭代器协议就通透了。
二、迭代协议的两个层次
Python 的迭代体系由两个独立的协议构成,很多文章把它们混为一谈,导致理解上的模糊。
2.1 可迭代对象(Iterable)
定义 :实现了 __iter__ 方法的对象。
__iter__ 方法的职责只有一个:返回一个迭代器对象。它本身不负责产生数据,只负责"产出一个知道怎么产生数据的对象"。
python
class NumberRange:
"""一个可迭代对象:表示一个整数范围"""
def __init__(self, start, stop):
self.start = start
self.stop = stop
def __iter__(self):
# 只负责返回迭代器,自己不持有迭代状态
return NumberRangeIterator(self.start, self.stop)
2.2 迭代器(Iterator)
定义 :同时实现了 __iter__ 和 __next__ 的对象。
__next__ 方法的职责:每次被调用返回序列中的下一个值 ,当序列耗尽时抛出 StopIteration。
迭代器同时也要实现 __iter__,且通常直接返回 self------这使得迭代器本身也可以被放入 for 循环。
python
class NumberRangeIterator:
"""NumberRange 的迭代器:持有当前状态"""
def __init__(self, current, stop):
self.current = current
self.stop = stop
def __iter__(self):
return self # 迭代器返回自身
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += 1
return value
测试两个类的配合:
python
rng = NumberRange(1, 5)
for n in rng:
print(n)
# 1
# 2
# 3
# 4
# 可以多次遍历(每次 __iter__ 都产生新迭代器)
for n in rng:
print(n)
# 1 2 3 4 再次正常输出
2.3 两者的本质区别
| 属性 | 可迭代对象 | 迭代器 |
|---|---|---|
| 必须实现 | __iter__ |
__iter__ + __next__ |
__iter__ 返回 |
新的迭代器 | self |
| 持有遍历状态 | 否 | 是 |
| 能否多次遍历 | 可以 | 不可以(状态耗尽) |
| 典型代表 | list、str、dict |
map、filter、zip 的返回值 |
三、for 循环的完整执行流程
否
是
否
是
for x in obj
调用 iter(obj)
即 obj.iter()
返回的对象
是否有 next?
抛出 TypeError:
'object is not iterable'
得到迭代器 it
调用 next(it)
即 it.next()
是否抛出
StopIteration?
将返回值
绑定到循环变量 x
执行循环体代码
循环正常结束
执行 else 子句(如果有)
值得注意的是,for 循环正常结束(即迭代器自然耗尽)时,才会执行 for...else 中的 else 子句。如果循环因为 break 中断,else 不会执行------这是 Python 中 for...else 的正确语义,常被误解。
python
# for...else 的实际含义演示
def find_prime(numbers):
for n in numbers:
if n > 1:
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
break # 找到因子,不是质数
else:
# 内层循环没有 break,说明 n 是质数
return n
return None
print(find_prime([4, 6, 8, 7, 9])) # 7
四、深入 CPython:iter() 做了什么
iter() 是一个内置函数,但它的行为比看起来复杂。完整的查找逻辑如下:
python
# iter(obj) 的等价 Python 伪代码
def iter(obj, sentinel=_missing):
if sentinel is not _missing:
# iter(callable, sentinel) 的两参数形式,稍后介绍
return CallableIterator(obj, sentinel)
# 查找 __iter__
iter_method = type(obj).__iter__
if iter_method is not None:
result = iter_method(obj)
# 严格检查返回值必须有 __next__
if not hasattr(type(result), '__next__'):
raise TypeError(
f"iter() returned non-iterator of type '{type(result).__name__}'"
)
return result
# 旧式序列协议(兼容性):如果有 __getitem__,用索引从 0 开始遍历
if hasattr(type(obj), '__getitem__'):
return IndexIterator(obj)
raise TypeError(f"'{type(obj).__name__}' object is not iterable")
旧式序列协议 (__getitem__)是一个很少被文档强调的细节:
python
class LegacySequence:
"""只实现 __getitem__,没有 __iter__"""
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
seq = LegacySequence([10, 20, 30])
# 仍然可以 for 循环!Python 会自动用索引迭代
for item in seq:
print(item)
# 10
# 20
# 30
# 原理:Python 会从 index=0 开始调用 __getitem__,
# 直到抛出 IndexError 为止(IndexError 被转换为 StopIteration)
这个兼容机制的存在,是因为 Python 2 时代大量代码只实现了 __getitem__ 而没有实现 __iter__------为了向后兼容,这套旧式协议被保留至今。
五、iter() 的两参数形式:一个被低估的特性
iter() 有一种两参数用法:iter(callable, sentinel)
python
iter(callable, sentinel)
- 第一个参数:一个无参可调用对象
- 第二个参数:哨兵值(sentinel)
- 行为:每次调用
callable(),当返回值等于sentinel时停止
典型场景:按块读取文件,直到遇到空内容:
python
from functools import partial
# 按 4096 字节读取二进制文件,直到返回空 bytes 为止
with open("large_file.bin", "rb") as f:
reader = partial(f.read, 4096)
for chunk in iter(reader, b""):
process(chunk) # 处理每个数据块
对比不使用 iter() 的写法:
python
with open("large_file.bin", "rb") as f:
while True:
chunk = f.read(4096)
if not chunk:
break
process(chunk)
两者等价,但前者把终止条件声明式地表达在 iter() 的参数里,意图更清晰。
另一个实际场景:从数据库按批次拉取数据:
python
import sqlite3
conn = sqlite3.connect("data.db")
cursor = conn.cursor()
cursor.execute("SELECT id, name FROM users ORDER BY id")
# 每次取 100 行,直到取完
batch_fetcher = partial(cursor.fetchmany, 100)
for batch in iter(batch_fetcher, []):
for row in batch:
process_user(row)
六、自定义迭代器的工程实践
6.1 可迭代对象与迭代器分离的设计价值
上面的 NumberRange 和 NumberRangeIterator 分别承担了不同职责------这是刻意为之的设计,不是多此一举。
分离的好处:可以同时有多个独立的迭代状态。
python
rng = NumberRange(1, 5)
it1 = iter(rng)
it2 = iter(rng) # 独立的第二个迭代器
print(next(it1)) # 1
print(next(it1)) # 2
print(next(it2)) # 1 ← it2 的状态完全独立于 it1
如果把迭代器状态放在可迭代对象自身(即 __iter__ 返回 self),则两个变量会共享同一个状态,嵌套遍历会产生非预期结果:
python
class BadRange:
"""反例:把状态放在自身,只能遍历一次"""
def __init__(self, start, stop):
self.current = start
self.stop = stop
def __iter__(self):
return self # 错误!返回自身意味着状态被共享
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += 1
return value
bad = BadRange(1, 4)
# 第一次遍历
for n in bad:
print(n) # 1 2 3
# 第二次遍历------什么也不输出!状态已耗尽
for n in bad:
print(n) # 无输出
设计原则 :如果对象需要被多次遍历,__iter__ 必须每次返回新的迭代器实例。只需要遍历一次的流式数据(如文件、网络连接),才可以让对象本身同时充当迭代器。
6.2 实战案例:数据库结果集分页迭代器
python
from typing import Iterator, Any
class PaginatedQuery:
"""
对任意数据库查询结果进行分页迭代。
惰性加载:只在需要时才执行下一页查询。
"""
def __init__(self, query_fn, page_size: int = 100):
"""
Args:
query_fn: 接受 (offset, limit) 参数的查询函数
page_size: 每页行数
"""
self.query_fn = query_fn
self.page_size = page_size
def __iter__(self) -> Iterator[Any]:
return PaginatedQueryIterator(self.query_fn, self.page_size)
class PaginatedQueryIterator:
def __init__(self, query_fn, page_size: int):
self.query_fn = query_fn
self.page_size = page_size
self.offset = 0
self._buffer: list = []
self._exhausted = False
def __iter__(self):
return self
def __next__(self):
# 缓冲区空了,去拉下一页
if not self._buffer:
if self._exhausted:
raise StopIteration
page = self.query_fn(self.offset, self.page_size)
if not page:
self._exhausted = True
raise StopIteration
self._buffer = list(page)
self.offset += len(page)
# 如果这页数据比 page_size 少,说明已到末尾
if len(page) < self.page_size:
self._exhausted = True
return self._buffer.pop(0)
# 使用示例(假设有一个 db 查询函数)
def fake_db_query(offset, limit):
"""模拟数据库查询:总共 250 条数据"""
all_data = list(range(1, 251))
batch = all_data[offset:offset + limit]
return batch
query = PaginatedQuery(fake_db_query, page_size=100)
total = 0
for row in query:
total += 1
print(f"共处理 {total} 条记录") # 共处理 250 条记录
# 再次遍历,得到相同结果
total2 = sum(1 for _ in query)
print(f"验证:{total2} 条") # 验证:250 条
七、内置迭代器的行为细节
理解内置类型的迭代行为,对写出正确代码至关重要。
7.1 列表迭代器是一次性的
python
numbers = [1, 2, 3]
it = iter(numbers)
print(list(it)) # [1, 2, 3]
print(list(it)) # [] ← 迭代器已耗尽!
# 注意和列表本身的区别:列表可以多次遍历
print(list(iter(numbers))) # [1, 2, 3] ← 从列表重新获取新迭代器
7.2 字典的迭代顺序
Python 3.7+ 起,字典保证按插入顺序迭代:
python
config = {"host": "localhost", "port": 5432, "db": "myapp"}
for key in config:
print(key)
# host
# port
# db
# 同时迭代键值
for key, value in config.items():
print(f"{key}: {value}")
dict.keys()、dict.values()、dict.items() 返回的是视图对象 (view),不是独立的列表。视图与字典内容同步,增删字典条目后视图也会变化------但不能在迭代字典时修改字典大小:
python
d = {"a": 1, "b": 2, "c": 3}
try:
for k in d:
if k == "b":
del d[k] # RuntimeError!
except RuntimeError as e:
print(e)
# dictionary changed size during iteration
# 正确做法:先收集要删除的键
to_delete = [k for k, v in d.items() if v == 1]
for k in to_delete:
del d[k]
7.3 文件对象是迭代器
文件对象本身就是迭代器(同时实现了 __iter__ 和 __next__),每次 next() 返回一行:
python
with open("data.txt", "r", encoding="utf-8") as f:
# f 既是可迭代对象也是迭代器
first_line = next(f) # 读取第一行
second_line = next(f) # 读取第二行
# 后续可以继续 for 循环,从第三行开始
for remaining in f:
print(remaining.rstrip())
文件对象没有把可迭代对象和迭代器分离------这是合理的,因为文件是流式数据,重置位置需要 seek(0),不像列表可以简单地从头开始。
八、协议检查与类型注解
8.1 运行时协议检查
python
from collections.abc import Iterable, Iterator
# 检查是否可迭代
print(isinstance([1, 2, 3], Iterable)) # True
print(isinstance("hello", Iterable)) # True
print(isinstance(42, Iterable)) # False
# 检查是否是迭代器
it = iter([1, 2, 3])
print(isinstance(it, Iterator)) # True
print(isinstance([1, 2, 3], Iterator)) # False(列表不是迭代器)
# 实用的防御性检查
def consume(obj):
if not isinstance(obj, Iterable):
raise TypeError(f"预期可迭代对象,收到 {type(obj).__name__}")
for item in obj:
process(item)
8.2 类型注解中的迭代相关类型
python
from collections.abc import Iterable, Iterator, Generator
from typing import TypeVar
T = TypeVar("T")
def flatten(nested: Iterable[Iterable[T]]) -> Iterator[T]:
"""将二维可迭代对象展平为一维迭代器"""
for inner in nested:
yield from inner
# 用法
result = list(flatten([[1, 2], [3, 4], [5]]))
print(result) # [1, 2, 3, 4, 5]
在类型注解层面:
Iterable[T]:可以被for循环的对象,产出T类型元素Iterator[T]:同时有__iter__和__next__的对象Generator[YieldType, SendType, ReturnType]:生成器(下一篇详细讲解)
九、迭代器协议的全景图
继承
继承
自身即迭代器
Iterable
+iter() : Iterator
Iterator
+iter() : Iterator
+next() : T
Generator
+send(value)
+throw(type, value, tb)
+close()
list
+iter() : list_iterator
str
+iter() : str_iterator
dict
+iter() : dict_keyiterator
file
+iter() : self
+next() : str
这张图揭示了迭代体系的继承关系:
- 所有迭代器都是可迭代对象(反之不成立)
- 生成器是迭代器的子集,额外提供了双向通信能力
- 内置类型各自有对应的专属迭代器类型
十、常见陷阱与最佳实践
陷阱一:把迭代器当可迭代对象共享
python
# 反例:在两处共享同一个迭代器
data = [1, 2, 3, 4, 5]
it = iter(data)
first_half = list(islice(it, 3)) # [1, 2, 3]
second_half = list(it) # [4, 5] ← 状态被消耗了
# 如果两处都需要完整数据,应该每次从可迭代对象获取新迭代器
first_half = list(islice(iter(data), 3))
second_half = list(islice(iter(data), 3, None))
陷阱二:zip 和 map 等惰性对象只能遍历一次
python
pairs = zip([1, 2, 3], ["a", "b", "c"])
print(list(pairs)) # [(1, 'a'), (2, 'b'), (3, 'c')]
print(list(pairs)) # [] ← 已耗尽!
# 如果需要多次使用,转成列表
pairs = list(zip([1, 2, 3], ["a", "b", "c"]))
陷阱三:StopIteration 在生成器内部被吞掉(Python 3.7+)
python
# Python 3.7+ (PEP 479):生成器内部的 StopIteration 会被转换为 RuntimeError
# 这是为了防止意外终止生成器
def gen():
it = iter([1, 2])
while True:
# 不能让 StopIteration 自然传播出去
# next(it) ← 这样写,当 it 耗尽时 StopIteration 会变成 RuntimeError
try:
yield next(it)
except StopIteration:
return # 在生成器里,用 return 来结束迭代
list(gen()) # [1, 2]
最佳实践小结
| 场景 | 推荐做法 |
|---|---|
| 数据源需要多次遍历 | 实现 __iter__ 返回新迭代器;不在自身存状态 |
| 流式数据(文件、网络) | 对象本身充当迭代器;__iter__ 返回 self |
| 无限序列 | 使用生成器或自定义迭代器;不要用列表存储 |
| 惰性计算结果 | 使用 map/filter/zip 等惰性工具;按需转列表 |
| 协议检查 | 用 isinstance(obj, collections.abc.Iterable) 而非 hasattr |
十一、与前序文章的联系
在 Python 进阶 #01 中分析了函数作为一等公民的底层结构。迭代器协议是 Python 数据模型(Data Model)的核心组成部分------__iter__ 和 __next__ 是双下划线方法(dunder method),它们不是语法糖,而是 Python 解释器与用户定义类之间的正式接口。
同样的设计思路贯穿整个 Python 对象系统:+ 运算符调用 __add__,len() 调用 __len__,for 调用 __iter__ 和 __next__。当定义了这些方法,自定义对象就自然融入 Python 的整个生态。
下一篇将在迭代器协议的基础上,深入探讨生成器(Generator)------它是迭代器协议的最优雅实现,也是 Python 处理大数据和流式场景的核心武器。
小结
- 迭代器协议 =
__iter__+__next__+StopIteration,for循环是这套协议的语法糖 - 可迭代对象 :有
__iter__,每次返回新迭代器;迭代器 :有__next__,持有状态,只能单向消费 iter()有旧式序列协议兼容(__getitem__)和两参数哨兵形式两个不常见用法- 设计多次遍历的数据源时,分离可迭代对象与迭代器是关键原则
- Python 3.7+ 起,生成器内部不能让
StopIteration自然传播(PEP 479)
如果这篇文章对迭代器协议的理解有所帮助,欢迎点赞收藏------这是持续输出高质量内容最直接的动力。技术文章写起来不容易,一个赞胜过千言万语。关注专栏,模块二"迭代器与生成器"的后续文章持续更新中。