结论先行:Python 的 with 不是语法糖,而是一套"自动收尾"协议——只要一个对象实现了 __enter__ 和 __exit__ 两个方法,就能进 with 块时自动"开门"、出块时不管有没有报错都自动"关门"。它解决的只有一个问题:资源别漏关。文件、锁、数据库连接、事务,凡是"用完了必须还原现场"的东西,**with** 都是最稳的写法。
新手常写:
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__,异常也照常调。
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。
每次手写类太重,标准库给你语法糖:
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 把函数劈成两半——前面是进门动作,后面是收尾动作。比写类清晰太多。
import threading
lock = threading.Lock()
with lock: # 进块自动 acquire,出块自动 release,绝不死锁
# 临界区
...
# 数据库:要么全提交,要么全回滚
with connection: # 出块自动 commit/rollback
cursor.execute("INSERT ...")价值:锁忘了 release 会死锁,事务忘了 commit/rollback 会悬挂连接。with 把"还原现场"变成编译器级别的保证。
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 怎么救 |
|---|---|---|
文件 | 句柄泄漏 | 自动 |
锁 | 死锁 | 自动 |
DB 连接 | 连接悬挂 | 自动 commit/rollback |
临时改状态 | 忘了还原 |
|
AttributeError: __enter__:你 with 的对象不是上下文管理器(没实现协议),检查是不是把普通对象套进去了。__exit__ 返回了 True,把它改回 False。with A() as a, B() as b: 一行叠多个,Python 支持。yield 后还想处理异常:@contextmanager 里 try/finally 包住 yield 即可。with 反而增加心智负担。一句话记住:with 就是 Python 的"自动关门器"——进块开门、出块关门,不管中途炸不炸,门一定关。凡是"用完必须还原"的东西,无脑用 with。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。