第 28 课 · 进阶 · 20 分钟

Python 上下文管理器

用 with 代码块保证准备和清理一定发生。

上下文管理器with__enter____exit__

小票打印机写到一半卡纸,文件却一直开着。with 块承诺即使主体出错也会收尾。上下文管理器到底保证了什么?

with 代码块会完成准备工作、执行主体,并保证即使主体抛出异常也会清理。文件是最常见的例子,但同一套协议也适用于锁、临时状态、数据库事务和测试夹具。 你还会抑制一个预期异常、在失败时清理,并管理数量不定的资源。

让 with 负责关闭文件

with 语句会自动调用文件的上下文管理器方法。代码块结束时文件会关闭,即使有异常向外传播也一样。不需要在每条路径上单独调用 close。

from pathlib import Path
path = Path("pypractice-context.txt")
path.write_text("hello\n", encoding="utf-8")
with path.open(encoding="utf-8") as handle:
    print(handle.read().strip())
path.unlink()

自动清理

理解 enter 和 exit

上下文管理器实现 __enter__ 来准备资源,实现 __exit__ 来接收异常信息并清理。with 语句会在主体前后调用它们。__exit__ 返回假值时,异常会继续向外传播。

class Guard:
    def __enter__(self):
        print("enter")
        return self
    def __exit__(self, exc_type, exc, tb):
        print("exit")

with Guard():
    print("inside")

上下文管理器协议

用生成器快速构造

contextlib.contextmanager 可以把生成器变成上下文管理器。yield 之前是准备,yield 之后是清理。yield 出来的值会绑定到 as 后面的变量。

from contextlib import contextmanager

@contextmanager
def label(name):
    print("start", name)
    yield name
    print("end", name)

with label("demo") as value:
    print("body", value)

紧凑的自定义管理器

__exit__ 可以抑制一个异常

从 __exit__ 返回 True 会抑制异常。只对范围很窄、确实预期的失败使用这种能力,例如可选文件不存在。对所有异常都返回 True 会隐藏 bug;contextlib.suppress 能用更少的代码表达同样意图。

class IgnoreMissing:
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc, tb):
        if exc_type is FileNotFoundError:
            print("ignored missing file")
            return True
        return False

with IgnoreMissing():
    raise FileNotFoundError("demo")
print("after")

只抑制预期类型

主体失败时也要清理

contextmanager 函数应把清理放在 yield 外面的 finally 里。正常退出和异常退出都会执行 yield 之后的代码,但前提是控制流回到生成器。把异常捕获放在 with 外面,能让清理和错误处理彼此分开。

from contextlib import contextmanager

@contextmanager
def session():
    print("open")
    try:
        yield "ready"
    finally:
        print("close")

try:
    with session() as state:
        print(state)
        raise RuntimeError("boom")
except RuntimeError as error:
    print("caught", error)

yield 放进 try,清理放进 finally

ExitStack 管理动态资源

资源数量要到运行时才知道时,ExitStack 可以逐个登记。代码块退出时,它会按相反顺序关闭。这样不必写出层数不定的 with 嵌套。

from contextlib import ExitStack
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as tmp:
    root = Path(tmp)
    with ExitStack() as stack:
        handles = []
        for name in ["a.txt", "b.txt"]:
            handle = stack.enter_context((root / name).open("w", encoding="utf-8"))
            handles.append(handle)
            handle.write(name)
        print(len(handles), "open")
    print("all closed")

创建资源时逐个登记

本课要点

  • with 在代码块退出时保证清理。
  • 上下文管理器实现 __enter__ 和 __exit__。
  • __exit__ 接收异常信息,也可以抑制异常。
  • contextlib.contextmanager 用一个 yield 把生成器变成管理器。
  • 窄范围抑制异常;finally 负责清理;ExitStack 管理动态资源。

常见错误

  • 打开文件却不用 with,并且在每条路径上都忘记关闭。
  • 以为只有没有异常时才会执行 __exit__。
  • 忘记 contextmanager 中 yield 之后的代码是在清理阶段运行。
  • 对所有异常都从 __exit__ 返回 True,把真正的 bug 藏起来。
轮到你

用 with 打开一个小文件,读取内容,并在代码块结束后删除它。

动手试试

改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。

浏览器内 Python本地运行 · 无需账号
首次运行会从 CDN 下载 Pyodide。修改后点击“运行代码”。

练习本课

12 道题,作答后立即看到解释。

查看 12 道题答案开始练习
← 装饰器dataclasses →