async with 必须在 async def 函数中使用,否则抛出 RuntimeError;__aenter__ 必须返回 awaitable,__aexit__ 必须是 async def 且按参数和返回值规则处理异常;手写类难点在于生命周期控制与失败兜底。

async with 语法报错:RuntimeError: async with outside async function
这是最常见的一踩就倒的坑——你写了 __aenter__ 和 __aexit__,但调用时没在 async def 函数里用 async with。
Python 要求所有 async with 必须出现在协程函数内部,否则直接抛出 RuntimeError。它不是语法错误,而是运行时检查,容易漏测。
- 必须把
async with MyAsyncContext() as x:放在async def函数里,不能放在模块顶层或普通函数中 - 如果只是想测试,用
asyncio.run()包一层最安全,比如asyncio.run(main()) - 别试图在同步函数里 await 异步上下文管理器——那得自己写事件循环调度,纯属自找麻烦
__aenter__ 返回值类型不对:awaitable 还是具体对象?
__aenter__ 必须返回一个 awaitable(比如协程、asyncio.Future),但它的 await 结果才是 as 后面绑定的值。这点和同步的 __enter__ 行为不一致,容易混淆。
常见错误是让 __aenter__ 直接返回资源对象(比如 return self.db_conn),结果 async with 报 TypeError: object X can't be used in 'await' expression。
立即学习“Python免费学习笔记(深入)”;
-
__aenter__应该返回self._connect()这类协程方法,而不是连接本身 - 如果初始化逻辑是同步的,也得包装成协程,比如
return asyncio.sleep(0, result=self) - 返回
awaitable是硬性要求;返回None或普通对象会立刻失败
__aexit__ 的参数顺序和返回值语义
异步版本的 __aexit__ 参数签名和同步版完全一样:__aexit__(self, exc_type, exc_value, traceback),但它本身必须是 async def,且返回值逻辑也一致:返回 True 表示已处理异常,不向上抛;返回 None 或 False 表示继续传播。
但注意:即使你 await 了清理操作,也不能靠它“吞掉”异常——异常处理与否,只看这个函数的返回值,跟 await 不 await 无关。
- 必须用
async def __aexit__,写成普通def会导致TypeError: object is not an awaitable - 如果清理操作可能失败(比如网络断开时关闭连接失败),建议在
try/except里做,但最终仍要明确返回True或False - 不要在
__aexit__里 raise 新异常——除非你清楚这会覆盖原始异常,且上层有对应处理
和 contextlib.asynccontextmanager 比,手写类更难在哪?
用 contextlib.asynccontextmanager 装饰一个生成器函数,比手写完整类简单得多。但手写类不可替代的场景是:需要复用状态、带初始化参数、或集成进已有类体系(比如数据库连接池里的连接对象本身就要支持异步上下文)。
难点不在语法,而在生命周期控制:比如 __aenter__ 失败后,__aexit__ 是否会被调用?答案是——不会。所以资源分配失败时,不能依赖 __aexit__ 做回滚,得在 __aenter__ 内部自己兜底。
- 如果
__aenter__中 await 失败(比如连接超时),Python 不会调用__aexit__,所以清理逻辑必须内聚在__aenter__里 - 类实例一旦进入
__aenter__,就不能假设它一定能走到__aexit__,状态管理要更谨慎 - 继承自
typing.AsyncContextManager只是类型提示,不提供任何运行时保障,别以为加了它就能少写逻辑


















