python之async with 深入详解

张开发
2026/6/20 23:11:50 15 分钟阅读
python之async with 深入详解
async with是 Python 异步编程里非常核心、也非常容易被“会用但没真正理解”的语法。很多人知道它常出现在aiohttp、数据库连接、锁、流式读写等场景中但对它的执行机制、异常传播方式、和普通with的本质区别并不清楚。这篇文章从语义、协议、执行流程、异常处理、实现方式和工程实践几个层面系统讲清楚async with。一、先说结论async with是什么一句话概括async with是“异步上下文管理器”的使用语法用来管理那些进入和退出都可能需要等待的资源。普通with适合这种场景打开文件加锁临时切换状态进入某个受控作用域而async with适合这种场景建立和关闭异步网络连接获取和释放异步锁打开和关闭异步会话进入和退出异步事务建立和清理异步流式资源核心差异不在“功能”而在“进入和退出过程本身是否可能挂起”。二、为什么普通with不够先回顾普通上下文管理器withopen(data.txt,r,encodingutf-8)asf:contentf.read()这里with做了两件事进入上下文时执行__enter__()退出上下文时执行__exit__()它们都是同步方法不能await。但是在异步程序中很多资源的初始化和清理并不是“立刻完成”的而是需要等待 I/O建立 TCP 连接要等申请连接池中的连接要等异步锁竞争要等提交/回滚事务要等关闭网络会话可能也要等如果还用同步with那么上下文进入和退出阶段就无法自然地表达这种“等待”。所以 Python 引入了异步上下文管理协议对应语法就是async with。三、async with的协议本质1. 普通with依赖的方法普通上下文管理器需要实现__enter__(self)__exit__(self,exc_type,exc,tb)2.async with依赖的方法异步上下文管理器需要实现__aenter__(self)__aexit__(self,exc_type,exc,tb)并且这两个方法都必须是可等待的通常写成async def。例如classAsyncResource:asyncdef__aenter__(self):print(进入上下文)returnselfasyncdef__aexit__(self,exc_type,exc,tb):print(退出上下文)returnFalse使用时asyncdefmain():asyncwithAsyncResource()asresource:print(正在使用资源)这里__aenter__()在进入代码块前被await__aexit__()在离开代码块时被await四、async with的执行流程看这段代码asyncwithmanagerasvalue:body()它的语义大致可以理解成manager_objmanager valueawaitmanager_obj.__aenter__()try:body()exceptExceptionasexc:suppressawaitmanager_obj.__aexit__(type(exc),exc,exc.__traceback__)ifnotsuppress:raiseelse:awaitmanager_obj.__aexit__(None,None,None)这段“展开理解”非常重要它解释了async with的全部关键行为。1. 进入阶段先执行valueawaitmanager_obj.__aenter__()这意味着进入上下文前允许发生异步等待事件循环可以在这里切换去执行别的任务2. 执行主体代码块进入async with的代码体后正常运行你的业务逻辑。3. 退出阶段无论代码块是正常结束还是抛出异常都会执行awaitmanager_obj.__aexit__(...)这和普通with的“保证清理”语义一致只不过清理动作本身也可以异步等待。五、async with和普通with的本质区别1. 共同点二者都用于“作用域资源管理”进入时获取资源离开时释放资源对异常进行统一处理2. 核心差异普通with进入和退出是同步的调用__enter__/__exit__async with进入和退出是异步的调用__aenter__/__aexit__只能出现在async def中3. 一个常见误区很多人以为“只要在异步函数里就都该用async with”。这是错的。是否使用async with取决于对象是不是异步上下文管理器不是取决于你当前是不是在异步函数里。例如在异步函数里仍然可以合法使用普通withasyncdefmain():withopen(data.txt,r,encodingutf-8)asf:contentf.read()这在语法上没有问题。只是这里的文件 I/O 仍然是同步阻塞的是否合适要看场景。六、最典型的使用场景1. 异步锁asyncio.Lock是最经典例子之一。importasyncio lockasyncio.Lock()counter0asyncdefworker():globalcounterasyncwithlock:tempcounterawaitasyncio.sleep(0.1)countertemp1asyncdefmain():awaitasyncio.gather(*(worker()for_inrange(10)))print(counter)asyncio.run(main())这里async with lock:的含义是进入时异步等待获取锁退出时自动释放锁如果写成手动形式大致是awaitlock.acquire()try:...finally:lock.release()async with让这类代码更安全也更不容易漏掉释放逻辑。七、网络会话中的async with以aiohttp为例importasyncioimportaiohttpasyncdeffetch():asyncwithaiohttp.ClientSession()assession:asyncwithsession.get(https://httpbin.org/get)asresponse:textawaitresponse.text()print(text[:100])asyncio.run(fetch())这里出现了两层async with第一层ClientSessionasyncwithaiohttp.ClientSession()assession:作用是管理 HTTP 会话对象的生命周期保证最终关闭连接池、释放底层资源。第二层请求响应对象asyncwithsession.get(...)asresponse:作用是管理单次请求的响应资源确保响应被正确关闭和回收。这个例子很能体现async with的价值网络资源不是纯内存对象它们往往伴随底层连接和 I/O 状态必须严谨清理。八、数据库事务中的async with在异步数据库库中async with常用于会话和事务管理。下面是概念化示例asyncdefcreate_user(db):asyncwithdb.transaction():awaitdb.execute(INSERT INTO users(name) VALUES(Alice))awaitdb.execute(INSERT INTO logs(message) VALUES(user created))它的语义通常是进入事务上下文时开始事务正常结束时提交事务出现异常时回滚事务也就是说__aexit__常常承载着“根据异常决定提交还是回滚”的责任。九、如何自己实现一个异步上下文管理器下面写一个完整示例模拟“连接远程资源”的生命周期。importasyncioclassAsyncConnection:def__init__(self,name):self.namename self.connectedFalseasyncdef__aenter__(self):print(f[{self.name}] 正在建立连接...)awaitasyncio.sleep(1)self.connectedTrueprint(f[{self.name}] 连接已建立)returnselfasyncdefsend(self,message):ifnotself.connected:raiseRuntimeError(连接尚未建立)print(f[{self.name}] 发送消息:{message})awaitasyncio.sleep(0.5)asyncdef__aexit__(self,exc_type,exc,tb):print(f[{self.name}] 正在关闭连接...)awaitasyncio.sleep(1)self.connectedFalseprint(f[{self.name}] 连接已关闭)returnFalseasyncdefmain():asyncwithAsyncConnection(server-1)asconn:awaitconn.send(hello)awaitconn.send(world)asyncio.run(main())这段代码体现了完整流程__aenter__负责异步初始化资源代码块内部使用资源__aexit__负责异步清理资源十、as后面的变量到底是什么看这段代码asyncwithAsyncConnection(server-1)asconn:awaitconn.send(hello)这里的conn是什么答案是conn就是await __aenter__()的返回值。例如classDemo:asyncdef__aenter__(self):return{status:ok}asyncdef__aexit__(self,exc_type,exc,tb):returnFalse那么asyncwithDemo()asvalue:print(value)输出就是{status:ok}所以as绑定的不是上下文管理器对象本身而是__aenter__()的返回值。十一、异常处理机制__aexit__的三个参数__aexit__签名通常是asyncdef__aexit__(self,exc_type,exc,tb):...这三个参数表示exc_type异常类型exc异常实例tbtraceback 对象如果代码块正常结束这三个值都是None。示例importasyncioclassLoggerContext:asyncdef__aenter__(self):print(进入上下文)returnselfasyncdef__aexit__(self,exc_type,exc,tb):print(退出上下文)print(exc_type ,exc_type)print(exc ,exc)returnFalseasyncdefmain():try:asyncwithLoggerContext():raiseValueError(something went wrong)exceptValueError:print(异常继续向外传播)asyncio.run(main())输出逻辑会表明__aexit__能观察到异常返回False时异常继续向外抛出十二、__aexit__返回值的意义这点很关键。1. 返回False或None表示不吞掉异常异常继续传播。asyncdef__aexit__(self,exc_type,exc,tb):returnFalse这是最常见、也最安全的做法。2. 返回True表示吞掉异常异常不会继续向外传播。classSuppressError:asyncdef__aenter__(self):returnselfasyncdef__aexit__(self,exc_type,exc,tb):returnexc_typeisValueError使用asyncdefmain():asyncwithSuppressError():raiseValueError(bad input)print(程序继续执行)这里ValueError会被抑制。3. 工程上应谨慎吞异常虽然技术上可以返回True但在工程代码里要非常谨慎。因为它可能让真正的问题被静默掩盖增加排查成本。十三、async with可以嵌套也可以并列1. 嵌套写法asyncwithresource_a()asa:asyncwithresource_b()asb:...2. 并列写法Python 也支持多个上下文管理器asyncwithmanager_a()asa,manager_b()asb:...它与嵌套形式语义接近通常更紧凑。但要注意代码可读性优先。如果每个资源意义都很强嵌套形式往往更清晰。十四、async with只能用在async def中这是语法要求。错误示例defmain():asyncwithsome_manager():pass会直接产生语法错误因为async with必须出现在异步上下文中。正确写法asyncdefmain():asyncwithsome_manager():pass然后通过事件循环运行importasyncio asyncio.run(main())十五、使用contextlib.asynccontextmanager简化实现如果你只是想快速写一个异步上下文管理器不一定要手写类和__aenter__、__aexit__。Python 标准库已经提供了更简洁的写法fromcontextlibimportasynccontextmanagerimportasyncioasynccontextmanagerasyncdefmanaged_connection(name):print(f[{name}] 建立连接)awaitasyncio.sleep(1)try:yield{name:name,connected:True}finally:print(f[{name}] 关闭连接)awaitasyncio.sleep(1)使用方式asyncdefmain():asyncwithmanaged_connection(server-2)asconn:print(conn)asyncio.run(main())这种写法怎么理解yield前面的部分相当于__aenter__yield后面的finally相当于__aexit__它非常适合写“获取资源 - 使用 - 清理资源”这一类逻辑尤其在工具函数、封装层中很常见。十六、asynccontextmanager的一个实用例子比如你想封装一个计时器同时允许里面执行异步代码fromcontextlibimportasynccontextmanagerimportasyncioimporttimeasynccontextmanagerasyncdeftimer(name):starttime.perf_counter()print(f{name}开始)try:yieldfinally:endtime.perf_counter()print(f{name}结束耗时{end-start:.3f}秒)asyncdefmain():asyncwithtimer(task-1):awaitasyncio.sleep(1.2)asyncio.run(main())这类模式在工程里很常见监控事务包装资源申请/释放统一日志埋点指标统计十七、async with和取消cancellation异步程序里还有一个重要现实协程可能被取消。例如某个任务运行中收到取消请求往往会抛出asyncio.CancelledError。这时async with的退出逻辑依然非常重要因为你仍然需要释放资源。示意代码importasyncioclassManagedLock:def__init__(self):self.lockasyncio.Lock()asyncdef__aenter__(self):awaitself.lock.acquire()print(锁已获取)returnselfasyncdef__aexit__(self,exc_type,exc,tb):self.lock.release()print(锁已释放)returnFalseasyncdefworker():asyncwithManagedLock():awaitasyncio.sleep(10)asyncdefmain():taskasyncio.create_task(worker())awaitasyncio.sleep(1)task.cancel()try:awaittaskexceptasyncio.CancelledError:print(任务已取消)asyncio.run(main())即便任务取消__aexit__仍然承担清理责任。这也是上下文管理器在异步场景中特别重要的原因之一。十八、几个常见误区1. 以为async with会让代码块内部自动并发不会。async with只负责“异步地进入和退出上下文”不负责让代码块内部自动变成并发执行。并发仍然由awaitasyncio.create_taskasyncio.gather任务调度这些机制决定。2. 在__aenter__里写阻塞代码错误示意classBadManager:asyncdef__aenter__(self):importtime time.sleep(3)returnself虽然这是async def但time.sleep(3)仍然会阻塞整个事件循环。正确做法应该是awaitasyncio.sleep(3)或者把阻塞操作移入线程池。原则很简单async with的异步协议只是给了你“可以等待”的能力不代表你自动不会阻塞。3. 忘记在__aexit__中做兜底清理如果一个异步上下文管理器负责底层资源__aexit__应该尽可能稳健。因为它是在“出错收尾”阶段执行的越到这里越要保证清理动作可靠。4. 把普通上下文管理器误当成异步上下文管理器错误示意asyncwithopen(data.txt)asf:...这是不行的因为open()返回的是普通文件对象它实现的是__enter__/__exit__不是__aenter__/__aexit__。十九、什么时候应该设计成async with一个资源对象适合设计成async with通常满足以下特征之一获取资源需要等待释放资源需要等待生命周期必须严格受控使用者不应该忘记清理异常时需要自动回滚、关闭、释放典型对象包括异步锁异步网络连接异步客户端会话异步数据库事务异步流限流器、配额器等需要进入/退出状态的对象如果进入和退出都完全同步、且成本很低就没有必要强行设计成async with。二十、一个对比手动写法 vsasync with手动写法awaitlock.acquire()try:print(临界区)finally:lock.release()async with写法asyncwithlock:print(临界区)二者语义等价但后者更清晰、更不容易出错。这也是上下文管理器存在的根本价值把资源生命周期控制从“人脑记忆”转成“语言结构保证”。二十一、理解async with的最佳思维模型可以把async with理解成一句很工程化的话我现在要进入一个受控的异步资源作用域进入前先异步准备资源离开时无论成功失败都异步清理资源。这个模型抓住了它的全部精髓不是单纯语法糖是资源生命周期管理工具强调作用域边界强调异常安全强调清理动作的可靠执行允许初始化和清理本身成为异步操作二十二、一个完整、可运行的综合示例下面给一个更接近实际工程的示例展示日志、异常传播和清理行为。importasynciofromcontextlibimportasynccontextmanagerasynccontextmanagerasyncdeffake_db_transaction():print(1. 打开数据库连接)awaitasyncio.sleep(0.3)print(2. 开始事务)awaitasyncio.sleep(0.2)try:yielddb-sessionexceptExceptionasexc:print(f3. 检测到异常准备回滚:{exc})awaitasyncio.sleep(0.2)print(4. 回滚完成)raiseelse:print(3. 提交事务)awaitasyncio.sleep(0.2)print(4. 提交完成)finally:print(5. 关闭数据库连接)awaitasyncio.sleep(0.2)print(6. 连接已关闭)asyncdefsuccess_case():asyncwithfake_db_transaction()assession:print(f使用会话:{session})awaitasyncio.sleep(0.5)print(业务执行成功)asyncdeffailure_case():asyncwithfake_db_transaction()assession:print(f使用会话:{session})awaitasyncio.sleep(0.5)raiseRuntimeError(写入失败)asyncdefmain():print( 成功场景 )awaitsuccess_case()print(\n 失败场景 )try:awaitfailure_case()exceptRuntimeErrorasexc:print(f外层捕获异常:{exc})asyncio.run(main())这个例子展示了async with在真实工程语义上的价值成功时提交失败时回滚最后总会关闭连接这类模式在数据库、消息队列、远程 API session、异步文件流中都极其常见。二十三、最佳实践1. 资源边界要尽量小不要把async with的作用域写得过大。资源占用时间越长竞争和泄漏风险越大。2.__aexit__默认不要吞异常除非你非常明确要抑制某类异常否则返回False或None。3. 不要在异步上下文管理器里混入阻塞操作例如time.sleep同步网络请求大量 CPU 密集任务这些都会破坏事件循环的调度能力。4. 用asynccontextmanager简化简单封装如果只是“前后包一层”通常没必要手写完整类。5. 明确文档语义如果你设计一个异步上下文管理器要清楚说明进入阶段做什么退出阶段做什么异常时如何处理是否会吞异常是否允许重复进入这对调用方很重要。二十四、总结async with不是一个“异步版 with”这么简单它本质上是 Python 为异步资源生命周期管理提供的语言级协议。它解决的是这样一个问题当资源的获取和释放本身都可能需要等待时如何仍然优雅、可靠、异常安全地控制作用域。你只要抓住以下几点基本就算真正理解了它async with依赖的是__aenter__和__aexit__两者都要可等待进入和退出阶段都允许await即使代码块抛异常也会执行退出逻辑__aexit__返回True可以吞掉异常但要谨慎它特别适合锁、连接、事务、会话、流等异步资源

更多文章