Python 的 with 到底在干啥?上下文管理器原理拆解,3 个场景看明白
结论先行:Python 的 with 不是语法糖,而是一套"自动收尾"协议——只要一个对象实现了 __enter__ 和 __exit__ 两个方法,就能进 with 块时自动"开门"、出块时不管有没有报错都自动"关门"。它解决的只有一个问题:资源别漏关。文件、锁、数据库连接、事务,凡是"用完了必须还原现场"的东西,with 都是最稳的写法。
1. 最常见的误用:忘了关文件
新手常写:
f = open('data.txt')
data = f.read()
f.close() # 万一上面报错,这行永远不执行 → 文件句柄泄漏
问题:read() 抛异常,close() 就被跳过,文件句柄一直占着。改成 with:
with open('data.txt') as f:
data = f.read()
# 无论块内是否报错,文件都自动关闭
原理:open() 返回的文件对象实现了 __enter__(返回文件自身)和 __exit__(负责关文件)。进块调 __enter__,出块调 __exit__,异常也照常调。
2. 自己写一个上下文管理器
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc_val, tb):
print(f"耗时 {time.time() - self.start:.3f}s")
return False # False=不吞掉异常,继续向外抛
with Timer():
sum(range(10**7))
要点:__exit__ 的三个参数是异常信息(exc_type/exc_val/tb),都是 None 表示正常退出;返回 True 会"吞掉"异常,返回 False 则放行。绝大多数情况返回 False。
3. 更省事的写法:@contextmanager
每次手写类太重,标准库给你语法糖:
from contextlib import contextmanager
@contextmanager
def tag(name):
print(f"<{name}>")
yield # yield 前的算 __enter__,后的算 __exit__
print(f"</{name}>")
with tag("div"):
print("hello")
# 输出:<div> hello </div>
为什么好用:yield 把函数劈成两半——前面是进门动作,后面是收尾动作。比写类清晰太多。
4. 真实场景:锁和数据库事务
import threading
lock = threading.Lock()
with lock: # 进块自动 acquire,出块自动 release,绝不死锁
# 临界区
...
# 数据库:要么全提交,要么全回滚
with connection: # 出块自动 commit/rollback
cursor.execute("INSERT ...")
价值:锁忘了 release 会死锁,事务忘了 commit/rollback 会悬挂连接。with 把"还原现场"变成编译器级别的保证。
5. 一个反直觉的点:with 块里的异常会进 exit
class Demo:
def __enter__(self): return self
def __exit__(self, t, v, tb):
print("exit, exc=", t)
return False
with Demo():
raise ValueError("boom")
# 先打印 exit, exc=ValueError,再向外抛
记住:__exit__ 是"善后"不是"拦截"。想静默吞异常才返回 True,否则异常照样抛。
对照表:with 能管什么
| 资源 | 不用 with 的隐患 | with 怎么救 |
|---|---|---|
| 文件 | 句柄泄漏 | 自动 close() |
| 锁 | 死锁 | 自动 release() |
| DB 连接 | 连接悬挂 | 自动 commit/rollback |
| 临时改状态 | 忘了还原 | __exit__ 还原 |
排错清单(收藏级)
AttributeError: __enter__:你with的对象不是上下文管理器(没实现协议),检查是不是把普通对象套进去了。- 异常被莫名吞掉:某个
__exit__返回了True,把它改回False。 - 想同时管多个资源:
with A() as a, B() as b:一行叠多个,Python 支持。 yield后还想处理异常:@contextmanager里try/finally包住yield即可。- 别滥用:纯计算、没有"资源要还原"的场景,用
with反而增加心智负担。
一句话记住:with 就是 Python 的"自动关门器"——进块开门、出块关门,不管中途炸不炸,门一定关。凡是"用完必须还原"的东西,无脑用 with。

浙公网安备 33010602011771号