python async with
就像你走进一家餐厅服务员对你说“请稍等我去拿菜单”然后就去忙别的了。等你点完菜他又说“请稍等我去下单”然后又消失了。这种“短暂离开再回来”的模式在编程里恰好对应着一种资源管理的办法——async with。很多人一开始看到这个关键字第一反应是“不就是with加了个async吗”其实还真不是这么简单。async with的诞生是因为传统with在处理那些需要等待的操作比如网络请求、文件读写、数据库连接时表现得像个急性子——它要求所有事情必须在当下立刻完成。而现实世界里的资源获取和释放往往需要等待比如等待数据库连接池给你分配一个连接或者等待操作系统确认文件已经写入磁盘。拿生活中的例子来说你平时用with打开一个普通文件就像自己去拿一个摆在桌上的笔伸手就拿得到。但async with处理的是什么呢就像你打电话订外卖你说“我要一份套餐”对方说“好的等会儿送到”。这时候你不能干等着可以去刷牙、洗脸、叠被子。async with就是这种“我先提出请求你去处理好了通知我”的写法。从本质上讲async with是对__aenter__和__aexit__这两个特殊方法的语法糖封装。任何对象只要实现了这两个方法就能被async with管理。你可能会想“这不是和with的__enter__、__exit__很像吗”确实区别就在于async with版本需要定义为async def而且你在里面可以await一些东西。这意味着进入和退出上下文的时候可以执行异步操作。它能做什么呢主要就是管理那些需要“请求-等待-获取”或者“关闭-等待-完成”的资源。最典型的就是网络连接。比如你用aiohttp做HTTP请求每次拿到一个session用完后要关闭。如果不用async with你可能得手动写await session.close()而且还得小心异常处理否则连接没关闭就会泄漏。用async with会自动在离开代码块时调用__aexit__把清理工作安顿好。另一个常见场景是数据库连接。比如用asyncpg连接PostgreSQL获取连接、执行查询、归还连接这一套流程用async with写出来特别自然。你甚至可以嵌套使用比如先获取一个连接再在连接上启动一个事务。关于怎么用基本的写法是这样的asyncwithsome_async_context_manager()asresource:# 使用resource这里some_async_context_manager()需要返回一个异步上下文管理器对象。比如aiohttp的ClientSessionasyncwithaiohttp.ClientSession()assession:asyncwithsession.get(http://example.com)asresponse:dataawaitresponse.text()注意这里嵌套了两个async with外面的管理session的生命周期里面的管理单个HTTP响应的生命周期。响应对象在离开内层async with后自动释放连接你不需要手动response.close()。还有一种用法是用asynccontextmanager装饰器来自定义异步上下文管理器。比如你想创建一个临时锁进入时获取锁退出时释放锁fromcontextlibimportasynccontextmanagerasynccontextmanagerasyncdeftemp_lock(lock):awaitlock.acquire()try:yieldfinally:lock.release()然后就可以用async with temp_lock(my_lock):了。这样写比手动acquire/release清晰得多也更容易避免忘记释放锁。说到最佳实践有几点值得特别注意。一是不要滥用async with。有些资源的管理其实不需要异步比如你对一个本地文件做简单读写用普通的with就行。如果硬套上async with反而会引入不必要的协程调度开销而且代码也变得更难理解。比如你只是读一个几十KB的配置文件没有必要异步。二是要注意async with中的异常处理。虽然__aexit__可以接收异常参数但默认情况下如果代码块里抛出了异常它会被传递到__aexit__里然后__aexit__可以选择吞掉异常、记录日志或者重新抛出。有些库的实现可能不太严谨异常处理不干净导致资源没释放。所以最好在async with块内自己捕获特定异常不要完全依赖上下文管理器来兜底。三是小心嵌套时的顺序问题。想象一下你打开两个资源async with A: async with B:退出的时候会先退出B再退出A。这个顺序在多数情况下是合理的但如果你依赖A在B之后存活就会出现问题。比如你先获取数据库连接池的连接(A)再在这个连接上开始一个事务(B)。如果退出时先退出了事务(B)再归还连接(A)那没问题。但如果顺序反了可能连接还没还回池里事务就提前关闭了。好在Python的上下文管理器是栈式的内层先退出所以通常没问题。但如果你手动用asyncio.gather或者asyncio.create_task来组合多个上下文管理器就得格外小心退出顺序。跟同类技术对比的话最直接的就是和with对比。with是同步的async with是异步的。一个简单的判断标准是如果进入或退出上下文管理器时有可能发生I/O等待比如网络、磁盘、等待锁那就用async with。如果没有等待用with更简单直接。另一种对比是跟手动await操作来对比。比如你不用async with而是写sessionaiohttp.ClientSession()try:respawaitsession.get(http://example.com)dataawaitresp.text()finally:awaitsession.close()这样写也能达到同样的效果但代码更冗长而且不同的库清理资源的API不统一有的叫close有的叫release有的叫disconnect。async with统一了资源管理的入口而且保证了即使发生异常也能正常清理。可以说它不仅简化了代码还提高了健壮性。还有人会拿async for来跟async with比较。它们其实是两种不同的东西async for用于迭代异步生成器比如从流中逐条读取数据。而async with用于管理资源生命周期。虽然都用了async前缀但语法规则的差别很大。如果你用过Go语言的defer或者Java的try-with-resources你会发现Python的async with在异步场景下是挺有特色的。Go的defer更灵活但需要你自己关心执行顺序而且没法在defer里直接处理异步操作除非结合WaitGroup之类的机制。Java的try-with-resources很简洁但它是同步的没有异步版本虽然Project Loom在探索类似的东西。Python的这个设计算是比较自然地把异步和资源管理糅合在了一起而且对新手也比较友好——async with看起来就像with的孪生兄弟降低了学习曲线。最后还有一个容易被忽视的点async with不只是用于网络和数据库这类远程资源。它也可以用于异步锁asyncio.Lock、信号量asyncio.Semaphore等同步原语。比如你想限制并发数量semasyncio.Semaphore(5)asyncwithsem:# 最多5个协程同时执行这里这样写比手动acquire/release清晰得多而且不会因为忘记释放锁而导致死锁。实际上asyncio.Lock、Semaphore、Condition等都实现了异步上下文管理器接口所以可以直接用async with。写代码的时候把async with当作一个惯例来用就好。只要是你在代码里看到“先打开、后关闭”或者“先请求、后释放”这样的模式而且涉及异步操作就可以考虑用async with来表达。它就像是一个代你善后的管家你只管在“with”块里干你的活它会默默处理好一切琐事。