__enter__和__exit__必须成对实现,因with语句强制调用二者;缺一即报AttributeError或TypeError,且__exit__中仅返回True可抑制异常。

为什么__enter__和__exit__必须成对实现
单独定义__enter__或__exit__没有意义——Python 的 with 语句在进入时调用__enter__,退出时**一定**调用__exit__,哪怕发生异常。如果缺__exit__,with会直接报 AttributeError: __exit__。
常见错误现象:TypeError: object does not support the context manager protocol,本质就是类没实现这两个方法,或拼写错误(比如写成_enter_少了一个下划线)。
实操建议:
-
__enter__返回值会绑定到as后的变量,若不需要暴露对象,可返回self或None -
__exit__(self, exc_type, exc_value, traceback)四个参数必须齐全,即使不使用也要保留占位;其中前三个为None表示未发生异常 - 若在
__exit__中返回True,会吞掉异常(不传播),这是显式抑制错误的唯一合法方式
如何让__exit__正确处理异常而不吞掉它
多数自定义上下文管理器不该吞异常——比如文件操作失败应该让调用方知道。但新手常误写 return True 或漏掉 return False(Python 默认返回 None,等价于 False,所以通常不用显式写)。
实操建议:
- 只在明确要屏蔽某类异常时才返回
True,例如忽略FileNotFoundError而继续执行 - 若需记录异常但不抑制,应在
__exit__中做日志,然后不返回或返回False - 避免在
__exit__中 raise 新异常,否则会覆盖原始异常;如真需转换,用raise new_exc from exc_value
示例:安全关闭资源,但不干扰业务异常
def __exit__(self, exc_type, exc_value, traceback):
self.close() # 总是执行清理
# 不 return,等价于 return False → 原异常照常抛出
用@contextlib.contextmanager替代手写__enter__/__exit__的适用场景
当逻辑简单、无状态、只需“进前 setup + 出后 teardown”时,@contextlib.contextmanager 更轻量。它把函数体拆成两段:yield 之前是 __enter__,之后是 __exit__。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
但要注意:装饰器版本无法直接访问实例属性,也不能复用已有类结构;且 yield 必须出现一次且仅一次。
实操建议:
- 适合一次性工具,比如临时修改环境变量、切换工作目录、打点计时
-
yield value的value就是as接收的对象;不写yield则as得到None - 异常会在
yield处抛出,yield后的代码仍会执行(类似finally),可用于清理
示例:临时设置超时
from contextlib import contextmanager
import signal
<p>@contextmanager
def timeout(seconds):
def timeout_handler(signum, frame):
raise TimeoutError("Operation timed out")
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(seconds)
try:
yield
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, old_handler)
</p>
自定义上下文管理器的性能与兼容性陷阱
看似只是语法糖,但不当实现会影响性能和跨版本行为。比如在 __enter__ 中做重操作(打开网络连接、读大文件),而调用方其实只在部分分支用到该资源,就浪费了。
实操建议:
-
__enter__应尽量轻量;重初始化可延迟到首次使用(配合属性缓存) - Python 3.11+ 支持
async with,但普通同步类不兼容;需另写__aenter__/__aexit__ - 若类同时继承多个具有上下文协议的父类,注意 MRO 顺序,避免
__exit__被意外覆盖 - 测试时务必覆盖异常路径:手动
raise触发__exit__,验证资源是否真正释放
最容易被忽略的是:上下文管理器本身不是线程安全的。如果同一个实例被多个线程共用,__exit__ 可能被并发调用,导致清理逻辑错乱——这种问题不会报错,但行为不可预测。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










