迭代器协议:`__iter__` / `__next__` 的完整执行流程

文章目录

    • [一、从一个看似普通的 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` 等惰性对象只能遍历一次](#陷阱二:zipmap 等惰性对象只能遍历一次)
      • [陷阱三:`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
持有遍历状态
能否多次遍历 可以 不可以(状态耗尽)
典型代表 liststrdict mapfilterzip 的返回值

三、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 可迭代对象与迭代器分离的设计价值

上面的 NumberRangeNumberRangeIterator 分别承担了不同职责------这是刻意为之的设计,不是多此一举。

分离的好处:可以同时有多个独立的迭代状态

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))

陷阱二:zipmap 等惰性对象只能遍历一次

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__ + StopIterationfor 循环是这套协议的语法糖
  • 可迭代对象 :有 __iter__,每次返回新迭代器;迭代器 :有 __next__,持有状态,只能单向消费
  • iter() 有旧式序列协议兼容(__getitem__)和两参数哨兵形式两个不常见用法
  • 设计多次遍历的数据源时,分离可迭代对象与迭代器是关键原则
  • Python 3.7+ 起,生成器内部不能让 StopIteration 自然传播(PEP 479)

如果这篇文章对迭代器协议的理解有所帮助,欢迎点赞收藏------这是持续输出高质量内容最直接的动力。技术文章写起来不容易,一个赞胜过千言万语。关注专栏,模块二"迭代器与生成器"的后续文章持续更新中。

相关推荐
AI科技星1 小时前
算法联盟ROOT · 全域数学物理卷第20、21、22分册:量子纠缠、隐形场论与时间膨胀
人工智能·算法·数学建模·数据挖掘·机器人
yuanpan1 小时前
Python + psutil 实战:开发一个简易系统监控工具
linux·运维·python
Android出海1 小时前
ChatGPT Image2 2.0正式上线:功能解析 + 使用教程(附提示词)
人工智能·ai·chatgpt·ai生图·chatgpt image2·images2
平凡但不平庸的码农1 小时前
Go Channel详解
开发语言·后端·golang
laomocoder1 小时前
Project-Nexus-WAN-跨公网Agent对话
开发语言·php
子安柠1 小时前
深入理解 Go 语言文件操作:从基础到最佳实践
开发语言·后端·golang
代码中介商1 小时前
C++文件流操作全解析
开发语言·c++
Forget_85501 小时前
RHEL——Kubernetes容器编排平台(二)
java·开发语言
Achou.Wang1 小时前
go语言中使用等待组(waitgroups)和内存屏障(barriers)进行同步
开发语言·后端·golang