必须实现 __enter__ 和 __exit__ 方法,否则 with 语句报 AttributeError;__enter__ 获取资源并返回绑定值,__exit__ 清理并可抑制异常。

为什么直接用类实例会报 AttributeError: __enter__
当你写 with MyClass() as obj: 却遇到 AttributeError: __enter__,说明 Python 在进入 with 块前,会严格检查实例是否实现了 __enter__ 和 __exit__ 这两个特殊方法。没定义?直接报错,不走任何 fallback 逻辑。
这不是语法糖失效,而是协议强制要求——with 的本质就是调用这两个方法,跟“是否是上下文管理器”完全等价。
必须实现的两个方法:__enter__ 和 __exit__
__enter__ 负责资源获取或事务开启,返回值会绑定到 as 后的变量;__exit__ 负责清理,接收异常三元组 (exc_type, exc_value, traceback),返回 True 可抑制异常(慎用)。
常见错误写法:
立即学习“Python免费学习笔记(深入)”;
- 只写了
__enter__没写__exit__→ 报错 -
__exit__参数少于 3 个 → 运行时报TypeError -
__exit__返回非布尔值(如None)→ 异常不会被抑制,但容易误以为“没生效”
正确骨架示例:
class DBTransaction:
def __init__(self, conn):
self.conn = conn
self._committed = False
<pre class="brush:php;toolbar:false;">def __enter__(self):
self.conn.begin()
return self # 或 return self.conn,取决于你想暴露什么
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
self.conn.commit()
self._committed = True
else:
self.conn.rollback()
return False # 不抑制异常实际事务场景中要注意的细节
数据库事务不是“开了就完事”,真实使用时得考虑连接状态、嵌套、超时、自动重试等。Python 的 with 本身不处理这些,全靠你手动控制。
关键点:
-
__enter__中抛异常 →__exit__仍会被调用,且exc_type是对应异常类型,可用于区分“开启失败”和“执行中失败” - 如果事务对象需支持多次进入(比如可重入),不能在
__enter__里重复begin(),否则可能报错或产生意外行为 -
__exit__中若再抛异常,会覆盖原始异常(除非你显式处理),调试时容易丢失根因 - 不要在
__exit__里做耗时操作(如网络请求、大文件写入),它本该轻量、确定性高
用 contextlib.contextmanager 替代手写更安全吗?
可以,但适用场景不同。@contextlib.contextmanager 适合简单、线性、无状态的资源管理(比如临时修改全局配置、打开单个文件)。它把函数体拆成 __enter__(yield 前)和 __exit__(yield 后),底层帮你处理了异常传播逻辑。
但事务类通常需要携带状态(如连接引用、已提交标记、重试计数),用装饰器写反而绕——你得把状态塞进闭包或外部变量,不如直接写类清晰可控。
一句话:有状态、需复用、要继承的事务管理,老老实实写类;纯一次性、无副作用的 setup/teardown,用 @contextmanager 更轻。
复杂点永远在状态同步和异常边界上:比如 commit() 失败了,__exit__ 里怎么回滚?回滚又失败了怎么办?这些没法靠语法糖解决,得你定义清楚语义。


















