__enter__和__exit__必须成对出现,因为with语句依赖二者完成资源获取与清理的完整生命周期;缺__exit__会报AttributeError,且无法保证异常路径下资源释放。
__enter__ 和 __exit__ 必须成对出现因为 with 语句的底层机制依赖这两个方法构成完整生命周期:进入时调用 __enter__ 获取资源,退出时无条件触发 __exit__ 做清理。缺一不可,否则会报 AttributeError: __exit__。
常见错误是只写了 __enter__(比如想简单返回个值),结果 with 块结束时找不到退出逻辑,资源泄漏或异常被吞掉。
__enter__ 应该返回要绑定到 as 变量的对象(可以是 self,也可以是其他实例)__exit__ 必须接收三个参数:exc_type、exc_value、traceback,即使你什么都不做也得声明它们__exit__ 返回 True,会抑制异常;返回 None 或 False 则异常照常抛出关键在 __exit__ 的返回值逻辑。不是“捕获异常”,而是“决定是否让异常继续向上冒泡”。比如日志记录后仍要抛出,就不能 return True。
典型场景:打开文件写入,希望 IO 错误暴露给调用方,但临时锁必须释放。
def __exit__(self, exc_type, exc_value, traceback):
self.lock.release() # 总要释放
if exc_type is not None and issubclass(exc_type, ValueError):
print("忽略 ValueError")
return True # 抑制这一类
return False # 其他异常照常抛出
exc_type is not None 判断是否有异常发生issubclass(exc_type, SomeException) 精确匹配类型,避免用字符串比较__exit__ 里主动 raise 新异常——这会覆盖原始异常,极难调试contextlib.contextmanager 装饰器替代手写类的适用边界当逻辑简单、无状态、不需复用实例时,@contextmanager 更轻量;但一旦涉及属性缓存、多次进入、或需要继承扩展,就必须用类。
例如临时切换工作目录,用函数式写法很干净:
from contextlib import contextmanager import os@contextmanager def cd(path): old = os.getcwd() os.chdir(path) try: yield finally: os.chdir(old)

yield 之前的部分相当于 __enter__,之后(finally 块)相当于 __exit__
@contextmanager 函数里返回值给 as 绑定——除非用 yield value,且该值会在 with 块中可用isinstance(obj, ContextManager) 检查(它只是个 generator function)不能只测正常流程,重点验证异常路径下资源是否释放。比如文件句柄、网络连接、数据库事务等,漏掉异常分支就等于埋雷。
最直接的方式是故意抛异常并检查状态:
with MyResource() as r:
assert r.is_open
raise KeyError("boom")
# 此处断言 r.is_open == False,否则 __exit__ 没生效
pytest.raises 包裹整个 with 块,再检查退出后状态__exit__ 里做耗时操作(如网络请求),它可能在异常处理链中被多次调用__exit__ 不依赖未初始化的属性——__enter__ 失败时 __exit__ 仍会被调用实际用起来最难的不是写两个方法,而是预判所有退出路径——正常结束、return、break、各种异常、甚至 os._exit()。只要没覆盖全,就存在资源残留风险。