小票打印机写到一半卡纸,文件却一直开着。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 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。