上下文管理器必须同时实现__enter__和__exit__方法,前者返回as绑定对象,后者在with块退出时必然执行资源清理,且需正确处理异常参数与幂等性。

__enter__ 和 __exit__ 方法必须成对实现
上下文管理器的核心是让类支持 with 语句,这要求必须定义 __enter__ 和 __exit__ 两个特殊方法。缺一不可——只写 __enter__ 会导致 AttributeError: __exit__;只写 __exit__ 则 with 无法获取上下文对象。
典型错误现象:RuntimeError: generator didn't yield(误用生成器写法但没配装饰器)或 TypeError: object does not support the context manager protocol。
-
__enter__应返回要绑定到as变量的对象(常为self,也可返回其他值) -
__exit__(self, exc_type, exc_value, traceback)的三个异常参数可能全为None(无异常时),不能省略或重排顺序 - 若在
__exit__中返回True,会抑制异常传播;返回False或None则正常抛出
资源清理逻辑必须放在 __exit__ 中,而非 __del__
__del__ 不可靠:它依赖垃圾回收触发,时机不确定,甚至可能在解释器关闭时才调用。而 __exit__ 在 with 块退出时**必然执行**,无论是否发生异常,这才是做释放、关闭、回滚的唯一可信位置。
常见使用场景:文件句柄、数据库连接、锁、临时目录、计时器启停。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
- 不要在
__exit__里 raise 新异常(除非有意替换原异常),否则会掩盖原始错误 - 避免在
__exit__中调用可能抛异常的复杂逻辑;如需容错,应自行捕获并记录 - 若资源已关闭/释放过,
__exit__应幂等处理(比如检查self._closed标志)
用 @contextlib.contextmanager 装饰器更轻量,但不适用于类实例管理
如果只是封装一段函数式逻辑(如临时修改环境变量、压栈/出栈状态),用 @contextlib.contextmanager + 生成器最简洁。但它返回的是一个上下文管理器对象,**不是类本身**,无法直接用于需要实例属性或方法调用的场景。
示例对比:
from contextlib import contextmanager
<p>@contextmanager
def timer():
start = time.time()
try:
yield
finally:
print(f"Elapsed: {time.time() - start:.2f}s")</p><h1>✅ 可以 with timer(): ...</h1><h1>❌ 不能 timer().start() 或访问实例属性</h1>
- 生成器中
yield之前的部分相当于__enter__,之后(finally)相当于__exit__ - 若需传参(如
timer(label="db")),必须写成工厂函数返回被装饰的生成器 - 类方式更适合需要维护状态、复用实例、或与其他类继承/组合的场景
测试上下文行为时,务必覆盖异常路径
很多人只验证正常流程(with MyCtx() as x:),却忽略异常退出时 __exit__ 是否真被调用、是否正确处理了 exc_type 等参数。这是最容易漏测也最易出问题的地方。
- 用
pytest.raises()或self.assertRaises()触发异常,再断言资源是否已释放(如文件是否 closed、锁是否 release) - 手动传入
exc_type=ZeroDivisionError等模拟异常,在单元测试中调用ctx.__exit__(...)直接测逻辑 - 注意:若
__exit__返回True但实际不该吞异常(比如仅处理IOError却吞了ValueError),会导致调试困难
真正难的不是写这两个方法,而是想清楚:哪些状态必须保证在退出时还原,以及异常发生时还原动作本身会不会失败。这两点决定了 <strong>exit</strong> 的健壮性,而不是语法是否正确。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










