直接写__enter__和__exit__易出错:__exit__返回值决定异常是否被吞(需显式return True),缺返回值导致None引发AttributeError,且三参数缺一不可;推荐用@contextmanager简化。

为什么直接写 __enter__ 和 __exit__ 很容易出错
不是加两个方法就能当上下文管理器用——__exit__ 的返回值会决定异常是否被吞掉,而多数人默认写 return None(等价于 return False),结果本想静默处理的异常反而抛出了。另外,__enter__ 返回值会被赋给 as 后的变量,如果忘了 return,就得到 None,后续操作直接 AttributeError。
-
__exit__必须接收三个参数:exc_type、exc_value、traceback,缺一不可,哪怕你只关心是否有异常 - 如果类实例本身需要在
with块中被使用,__enter__就得return self;如果要返回新对象(比如文件句柄),就得明确构造并返回 - 不要在
__exit__里手动调用raise,除非你清楚自己在压制已有异常并抛出新的——这会覆盖原始 traceback
怎样让 __exit__ 正确吞掉特定异常
常见需求是:打开资源 → 执行可能出错的操作 → 出错了就清理并静默忽略,不出错就正常收尾。关键在 __exit__ 的返回值:返回 True 表示“我已处理该异常,别往外传”,返回 False 或 None 表示“请继续向上抛”。
def __exit__(self, exc_type, exc_value, traceback):
self.cleanup()
if exc_type is ValueError:
return True # 吞掉 ValueError
return False # 其他异常照常抛出
- 检查
exc_type比检查exc_value更安全,避免因字符串内容或属性差异误判 - 不要写
if exc_value and "timeout" in str(exc_value):—— 类型不匹配、None、自定义异常无__str__都会崩 - 如果从不打算吞异常,显式写
return False,比留空更清晰
用 contextlib.contextmanager 替代手写协议更省心
90% 的简单场景,不需要写完整类——用装饰器函数更轻量、不易漏参数、自动处理返回逻辑。它把生成器函数转成上下文管理器,yield 之前的部分是 __enter__,之后是 __exit__(含异常处理)。
from contextlib import contextmanager <p>@contextmanager def db_transaction(conn): conn.begin() try: yield conn # 相当于 <strong>enter</strong> 返回值 except Exception: conn.rollback() raise else: conn.commit()</p>
- 不用手动定义类,避免
self绑定和生命周期管理失误 -
yield后的代码天然在finally语义下执行,无论是否异常都走清理逻辑 - 如果想吞异常,把
except块里的raise去掉即可,比判断exc_type更直观
类中同时支持实例复用和独立生命周期的陷阱
如果一个类既想被多次 with 使用,又希望每次 with 都有干净状态(比如连接池中的某个连接),就不能在 __init__ 里预分配资源,也不能让 __enter__ 总是返回同一个对象。
立即学习“Python免费学习笔记(深入)”;
- 错误做法:
self.conn = connect()在__init__,然后__enter__直接return self—— 多个with块共享同一连接,可能并发冲突 - 正确做法:在
__enter__中建立新连接或从池取可用连接,__exit__归还或关闭;实例本身只是工厂,不持有长期资源 - 如果真要复用实例(如配置对象),确保
__enter__和__exit__不修改其核心状态,否则第二次with可能拿到脏数据
真正难的不是写这两个方法,是想清楚:这个上下文管理器的“生命周期边界”到底在哪——资源何时创建、谁负责释放、异常时清理能否失败、多个上下文之间要不要隔离。这些设计问题没理清,代码越写越像补丁。


















