Python 上下文管理器与 with 语句
更新时间:2026-08-30。本文回答:
with open(...) as f到底做了什么?为什么它能保证文件一定关闭?怎么自己写一个支持with的对象? 这是 Python 写"不漏资源"工程代码的核心机制。
一、问题:资源忘了释放会怎样
凡是"打开就要关闭"的东西——文件、socket、数据库连接、锁、临时目录——一旦忘了释放,就会泄漏。传统的 try/finally 能解决,但啰嗦且容易漏写:
f = open("data.txt")
try:
data = f.read()
finally:
f.close() # 忘了这行,文件句柄就一直占着with 语句把"进入时准备、退出时清理"固化成语法,保证无论是否抛异常,清理代码都会跑:
with open("data.txt") as f:
data = f.read()
# 离开这个块,f 一定被关闭,哪怕中间抛了异常二、协议:__enter__ 与 __exit__
任何支持 with 的对象,只需实现两个方法:
__enter__(self):进入with块时调用,返回值赋给as后的变量。__exit__(self, exc_type, exc_val, exc_tb):离开块时调用;返回真值表示"吞掉异常",否则异常继续向上抛。
class Timer:
def __enter__(self):
import time
self.t = time.time()
return self
def __exit__(self, et, ev, tb):
import time
print(f"耗时 {time.time()-self.t:.3f}s")
with Timer():
sum(range(10_000_000)) # 自动打印耗时核心结论:上下文管理器的价值不在"省几行",而在把"资源生命周期"和"业务代码"绑死——只要进了 with,清理就有保证,这和本站"可观测、可预期"的工程纪律一致。
三、更轻的写法:@contextmanager
每次写类太重。标准库 contextlib 用生成器 + 装饰器一行搞定:把 yield 前的代码当 __enter__,yield 后的当 __exit__。
from contextlib import contextmanager
@contextmanager
def open_db():
conn = connect() # 等价 __enter__
try:
yield conn # 块内拿到 conn
finally:
conn.close() # 等价 __exit__,异常也走
with open_db() as db:
db.query("...")- 用
try/finally包住yield,保证清理在异常时仍执行。 - 适合"成对操作":加锁/解锁、开事务/提交或回滚、设环境变量/还原。
四、实战 demo:锁的自动释放
多线程里忘了释放锁会死锁。用上下文管理器包一层,进出自动加解锁:
import threading
lock = threading.Lock()
def worker():
with lock: # 进入加锁,离开解锁(含异常路径)
# ... 临界区 ...
pass对比 Go 的 defer mu.Unlock()(见Go 并发模型)和 C++ 的 RAII(构造加锁、析构解锁),三者解决的都是同一件事:资源释放和作用域绑定,不靠人记得调。
五、实战 demo:计时与性能剖析接入
把 性能优化路径 里的计时,做成可复用的上下文管理器,顺手还能接 perf:
import time
from contextlib import contextmanager
@contextmanager
def profile(name):
t = time.time()
try:
yield
finally:
dt = time.time() - t
print(f"[profile] {name}: {dt*1000:.1f}ms")
with profile("load_config"):
cfg = load_config() # 业务无感,自动出耗时实测:对一段 1 千万次 sum(range(...) 的调用,profile 包裹后额外开销约 1~2 微秒(仅两次 time.time() 调用),对秒级任务可忽略。
六、与本站主线的衔接
| Python 机制 | 本站对应 | 衔接文档 |
|---|---|---|
| 资源生命周期绑定作用域 | RAII 与智能指针 | C++ 同构思想 |
with 异常安全 | 崩溃与信号 | 异常/信号下的资源清理 |
| 锁自动释放 | Go 并发模型 | defer 对照 |
| 计时接 perf | perf 观测方法论 | Python 层热点定位 |
七、常见坑
__exit__返回真值会吞异常:想让异常继续抛,就return None或干脆不 return;别手滑return True把 bug 藏了。- 生成器写法忘了
try/finally:@contextmanager里若yield后没包finally,块内抛异常时清理不执行。 yield只能有一次:@contextmanager不支持多yield,多资源用ExitStack(contextlib.ExitStack)。
一句话总结
with 把"进入准备、退出清理"绑成语法保证——__enter__/__exit__ 是协议、@contextmanager 是生成器捷径,它和 C++ RAII、Go defer 解决同一件事:让资源释放不依赖人的记性。
继续:GIL、多进程与 asyncio → 性能优化路径。