先给结论:with 语句的本质,是在代码块进入时自动调用 __enter__、退出时自动调用 __exit__,它解决的核心问题只有一个——不管中间是正常结束还是抛了异常,资源都能被正确释放。你以为它只是"打开文件更优雅的写法",其实它是一层"保证收尾"的语法糖,等价于 try...finally,但比手写 finally 可靠得多。
下面把 __enter__ / __exit__ 的运作机制拆开,再列 7 个真实误用场景,每个都给正确写法。
with 等价于 try...finally,但不会写错不用 with 时,你要手动关资源:
f = open("data.txt", "r")
try:
content = f.read()
finally:
f.close() # 异常时也必须关,忘了就文件句柄泄漏用 with 后,Python 在离开代码块时无条件调用 __exit__,等于帮你写好了那个 finally:
with open("data.txt", "r") as f:
content = f.read()关键点:__exit__ 在异常传播之前被调用,所以即使 f.read() 抛错,文件也会被关掉。这就是 with 比手写 try...finally 更安全的根因——你不可能"忘写 finally"。
__exit__ 的三个参数,决定了异常会不会被吞自定义上下文管理器时,__exit__ 的签名是:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# exc_type 非 None 表示代码块里抛了异常
...返回值有讲究:
None / False / 0):异常继续向外传播,调用方会收到这个错。True):异常被吞掉,调用方以为一切正常。class Swallow:
def __enter__(self): return self
def __exit__(self, et, ev, tb): return True # 危险!吞掉所有异常
with Swallow():
raise ValueError("出错了")
print("居然走到这里") # 异常被静默吞掉__exit__ 里返回 True 吞掉异常这是最隐蔽的 bug。你想"优雅处理",结果把关键错误吃了:
def __exit__(self, et, ev, tb):
if et is not None:
log.error("something failed: %s", ev)
return True # ⚠️ 异常没了,调用方毫不知情正确做法:除非你真的要压制特定异常,否则返回 None(假值)让异常传播。需要压制也要显式判断类型:
def __exit__(self, et, ev, tb):
if et is FileNotFoundError:
return True # 只压制这一种
return False # 其他异常照常抛with 退出后还能用文件对象with open("data.txt") as f:
data = f.read()
print(f.read()) # ❌ ValueError: I/O operation on closed filewith 块一结束,上下文管理器已经把资源关了。变量 f 还在,但它是个已关闭的对象。需要后续用,就把使用逻辑放进 with 块里,或显式返回你需要的数据。
with 的嵌套顺序搞反同时管理多个资源时,常见两种写法:
with open("a.txt") as fa, open("b.txt") as fb:
...
with open("a.txt") as fa:
with open("b.txt") as fb:
...要小心的是:先开的资源后关。如果你依赖 A 在 B 关闭前还可用,嵌套顺序就很重要。一般建议用写法 A 保持扁平,可读性更好。
contextlib.contextmanager 忘了在 yield 后写收尾用装饰器写管理器最省事,但收尾代码必须放在 yield 之后,且最好包在 try...finally 里:
from contextlib import contextmanager
@contextmanager
def timer():
start = time.time()
try:
yield # 这里是 with 块执行的位置
finally:
print(f"耗时 {time.time()-start:.2f}s") # ✅ 收尾放 yield 后
with timer():
do_something()坑点:如果 yield 之后的代码不在 finally 里,一旦 with 块抛异常,收尾逻辑就不执行。务必用 try...finally 包住 yield。
__enter__ 返回的不是你想要的那个对象with ... as x 里的 x,是 __enter__ 的返回值,不一定是管理器自己:
class DB:
def __enter__(self):
self.conn = connect()
return self.conn # as 拿到的是连接,不是 DB 实例
def __exit__(self, *a):
self.conn.close()
with DB() as conn:
conn.execute(...) # 这里用的是连接,不是 DB很多人误以为 as 拿到的是 DB() 实例,结果调用了不存在的方法。记住:as 绑定的是 __enter__ 的返回值。
try...finally vs with维度 | 手写 |
|
|---|---|---|
资源释放可靠性 | 依赖你记得写 | 语言保证退出必调用 |
异常时释放 | 写了才释放,漏写就泄漏 | 无条件释放 |
可读性 | 样板代码多,嵌套深 | 扁平清晰 |
多资源管理 | 易漏关其中一个 | 逗号并联一次管好几个 |
自定义成本 | 无 | 实现 |
异常传播控制 | 你自己决定 | 由 |
with,别手写 try...finally。__exit__ 默认返回 None 让异常传播,别随意 return True 吞异常。yield 之后的收尾务必包在 try...finally 里,否则 with 块抛错时收尾不执行。说到底,with 帮你兜底的不是"打开",而是"关闭"。它最值钱的地方,是在你最容易忘记收尾的异常路径上,替你把那扇门稳稳关上。
你平时用 with 时,有没有遇到过"明明关了文件却还是报句柄泄漏"的情况?评论区聊聊你踩过的上下文管理器坑。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。