KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

07 · 迭代器协议与上下文管理器:for 和 with 的底层 — keel 龙骨

## 这一章解决什么问题

这一章解决什么问题

for 和 with 天天用,但「for 循环底层发生了什么」「with 到底保证什么」多数人答不上来。这一章讲两个协议——迭代器协议和上下文管理器协议。前者是 yield 课的直接前置:不先懂 __iter__/__next__,yield 的所有行为都像魔法;懂了它,yield 只是把协议手工实现换成语法糖。

for 循环的底层翻译

先看现象:任何能被 for 遍历的对象,都不是「天生能 for」,而是实现了协议。Python 把

for item in collection:
    print(item)

翻译成:

it = iter(collection)          # 1. 拿迭代器:调 collection.__iter__()
while True:
    try:
        item = next(it)        # 2. 取下一个:调 it.__next__()
    except StopIteration:      # 3. 取到头会抛 StopIteration
        break                  #    for 把这个异常当成"正常结束"信号,静默 break
    print(item)

三个关键角色:

用 list 验证「可迭代 ≠ 迭代器」:

>>> nums = [1, 2, 3]
>>> it = iter(nums)
>>> next(it)      # 1
>>> next(it)      # 2
>>> for x in nums: pass   # nums 完好,还能再遍历——它是可迭代对象
>>> next(it)      # 3
>>> next(it)      # StopIteration —— it 这个游标已经耗尽

list 可以无限次 for;迭代器只能消费一次。 这是「迭代器 vs 序列」的分界,也是后续所有「为什么我的数据第二次遍历就空了」问题的答案。

手写一个迭代器:看清协议全貌

不依赖任何语法糖,手写一个倒计时迭代器:

class Countdown:
    """可迭代对象:每次 iter() 返回一个新游标"""
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        return CountdownIterator(self.start)   # 新游标,可重复遍历

class CountdownIterator:
    """迭代器:一次性游标"""
    def __init__(self, current):
        self.current = current

    def __iter__(self):
        return self                 # 迭代器返回自己

    def __next__(self):
        if self.current <= 0:
            raise StopIteration     # 信号:没货了
        self.current -= 1
        return self.current + 1

>>> for n in Countdown(3): print(n)
3
2
1

请对着这段代码记住三件事:__iter__ 管发货(给游标),__next__ 管货品(给下一个值),StopIteration 管收摊。下一章 yield 会把这个类压缩成三行——到时候你会回来对照:yield 替你写了 __next__ 的全部机械部分。

生成器:协议的语法糖(预告)

同样的倒计时,用 yield 写:

def countdown(start):
    while start > 0:
        yield start          # 暂停在这里,交出一个值
        start -= 1

>>> for n in countdown(3): print(n)
3
2
1

countdown(3) 没有执行函数体——它返回了一个生成器对象(迭代器的一种)。for 每次要值时函数才向前跑到下一个 yield。你手写的 CountdownIterator 里的状态(self.current)和 __next__ 机械(判断、递减、返回、抛信号),全部由 yield 的执行模型代管。

这一章只需要记住结论:生成器 = 自动实现迭代器协议的函数。 它为什么能「暂停—恢复」、send 怎么往里传值、异常怎么往里扔——正是 yield 课 01/02 章的全部内容。

上下文管理器协议:with 到底保证什么

with 的承诺很具体:无论代码块正常结束还是中途抛异常,__exit__ 都会被调用。

with open("data.txt") as f:
    process(f.read())
# 等价于:

f = open("data.txt")
try:
    process(f.read())
finally:
    f.close()          # 文件在异常时也会被关——这就是 with 的全部意义

所以「with 管资源清理」的准确说法是:with 把「用完必须清理」从纪律变成语法。忘写 finally 不会忘写 with。

自己实现一个(协议就两个方法):

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self                  # as 后面接到的就是这个返回值

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self.start
        print(f"took {self.elapsed:.3f}s")
        return False                 # False = 不吞异常,继续传播

with Timer() as t:
    heavy_work()

__exit__ 的三个参数是「代码块里炸的异常信息」(没炸就是三个 None)。返回值有语义:返回 True 会吞掉异常,返回 False/None 继续传播。默认写 None(别吞),需要「拦截特定异常」时才显式返回 True——这也是 contextlib.suppress 的原理。

contextlib:95% 的场景不用写类

日常最常用的是 @contextmanager——用生成器写上下文管理器:

from contextlib import contextmanager

@contextmanager
def timer(label):
    start = time.perf_counter()     # __enter__ 部分
    try:
        yield                        # ← with 块的执行发生在这一刻
    finally:
        elapsed = time.perf_counter() - start   # __exit__ 部分
        print(f"{label}: {elapsed:.3f}s")

yield 之前是进入逻辑,yield 之后是退出逻辑,yield 本身代表 with 块。是不是发现 yield 又出现了?它是 Python 里「函数能在中间暂停」的唯一原语——本课用它简化了协议书写,yield 课把它拆到底层。这个「一个原语、两种用途」正是 yield 值得单独开一门课的原因。

配套的两个小工具也值得记住:

from contextlib import suppress, ExitStack

with suppress(FileNotFoundError):    # 明确声明"这个异常我知道,吞掉"
    os.remove(tmp_file)

with ExitStack() as stack:           # 动态管理任意多个上下文
    files = [stack.enter_context(open(p)) for p in paths]
# 退出时按逆序全部关闭

自检三题

  1. for x in [1,2] 循环两次后,再 for x in [1,2] 还能遍历吗?换成 iter([1,2]) 存下来的迭代器呢?
  2. 手写迭代器类时,为什么 __iter__ 要返回 self?Countdown 里为什么却返回新对象?
  3. @contextmanager 装饰的函数里,try/finally 的 finally 部分对应 __exit__ 的什么行为?如果把 finally 去掉会怎样?

下一站

yield 课(python-yield-deepdive):本章的迭代器协议是它的直接前置。从 yield 的暂停/恢复逐行演算开始,到 send/throw/close 双向通道,再到异步生成器——你将看到本章所有「语法糖」背后的完整机制。

进入 keel 阅读