with是 Python 的资源管理语法糖:把「进入前准备、退出时清理」封装成对象协议,异常路径上也保证释放。本文拆解其底层机制(__enter__/__exit__)、典型场景与两种自定义方式。
从资源泄漏说起
f = open("test.txt", "r")
data = f.read()
f.close() # 如果read中间抛异常,close不会执行!read() 中途抛异常,f.close() 就永远执行不到,文件描述符泄漏。try-finally 能兜住,但每个资源都要手写一遍样板;with 把这段样板收进对象协议,交给解释器执行——就算内部代码抛异常,退出时的清理逻辑依然一定会执行:
with open("test.txt", "r", encoding="utf-8") as f:
data = f.read()
# 离开代码块,自动调用f.close(),哪怕中间报错
print(data)open() 返回的文件对象本身就是上下文管理器:__enter__ 返回文件句柄赋给 f,退出块时自动 f.close()。
运作机制:enter 与 exit
with 用来管理上下文资源,本质是封装了两件事:进入上下文前执行准备操作,退出上下文时自动执行清理操作。底层依赖对象的两个魔法方法(特殊方法 协议成员):
__enter__():进入with代码块之前调用;返回值会赋值给as后的变量__exit__(exc_type, exc_val, exc_tb):离开代码块一定调用(正常退出 / 异常退出都会触发)- 参数:异常类型、异常值、异常栈;无异常则全部为
None - 返回 True:压制异常;返回 False / None:向外抛出异常
- 参数:异常类型、异常值、异常栈;无异常则全部为
伪代码等价:
# with obj as var: ...
obj = 资源对象
var = obj.__enter__()
try:
执行with内部代码
finally:
obj.__exit__(...) # 无论是否报错,都会执行Note
with= 自动 try-finally,保证资源释放。__exit__能拿到异常信息,但默认不吞异常——只有显式返回True才压制。
典型使用场景
- 文件 IO:自动 close 文件
- 锁:自动 acquire / release 线程锁
- 数据库连接:自动关闭连接
- PyTorch:
torch.amp.autocast()混合精度上下文管理器
from torch.amp import autocast
# 进入上下文:开启自动混合精度
with autocast(device_type="cuda", dtype=torch.float16):
pred = model(x)
loss = loss_fn(pred, y)
# 退出上下文:关闭autocast,恢复默认精度autocast 就是上下文管理器:__enter__ 标记后续算子优先使用指定半精度,__exit__ 退出混合精度环境。前向传播中途报错,也会正常退出 autocast 上下文、不残留状态——这正是「__exit__ 必然执行」的直接收益。autocast 背后的 AMP 机制(算子按精度分流、与 GradScaler 配合)在 torch.cuda 与混合精度 中展开。
自定义上下文管理器
手写协议:
class MyContext:
def __enter__(self):
print("===进入上下文__enter__===")
return 100 # as后面变量拿到这个返回值
def __exit__(self, exc_type, exc_val, exc_tb):
print("===退出上下文__exit__===")
if exc_type:
print(f"捕获到异常:{exc_val}")
# 返回None,异常继续向外抛出
with MyContext() as val:
print(f"val = {val}")
# raise RuntimeError("测试异常") # 就算抛异常,__exit__依旧执行
print("代码结束")不写 class 也能做:contextlib.contextmanager 装饰器用生成器快速实现,yield 前是准备逻辑、后是清理逻辑:
from contextlib import contextmanager
@contextmanager
def my_ctx():
print("准备工作(enter)")
yield 200 # yield的值赋值给as变量
print("清理工作(exit)")
with my_ctx() as v:
print(v)执行顺序:执行到 yield 暂停、返回值交给 as 变量 → 执行 with 内部代码 → 退出块,继续执行 yield 后面的清理代码。
Tip
简单的准备/清理配对用
@contextmanager生成器最省事;进入时要做复杂初始化、或对象本身有生命周期语义(连接、会话)时用 class 协议。
多个上下文可以同时声明,按书写顺序进入、逆序退出:
with open("a.txt") as f1, open("b.txt") as f2:
pass边界与陷阱
as var不是必须,有些上下文管理器不需要返回值,可以省略as xxxwith autocast(device_type="cuda"): pred = model(x)__exit__依然会收到异常信息,但默认不会吞异常;只有__exit__返回True才会压制异常with作用域只在缩进块;缩进结束就退出上下文- 不要混淆:
with只是资源管理语法糖(见 Python 语法糖),不是异常捕获——除非你在__exit__里处理
判断一个对象能不能 with,看它有没有实现这对协议;写自己的资源类时,把清理逻辑放进 __exit__ 而不是依赖调用方记得 close——「忘记释放」从运行时隐患变成结构上不可能出错。