python async for
# 深入理解Python的async for:异步编程中的迭代革命
写Python异步代码时,有个东西很容易被忽略,但用好了能让代码质量提升一个档次——那就是async for。先别急着说“不就是个for循环加个async嘛”,这玩意儿背后的设计哲学和使用场景,远比表面上看起来要丰富。
它是谁?一个会“等待”的迭代器
async for本质上是一种异步迭代器。普通迭代器是同步的,生成一个元素就得等着处理完才能拿下一个。比如我们平时遍历列表:
foriteminlist:process(item)这个过程是阻塞的——list里的元素必须全部准备好,或者遍历时得等process执行完才能继续。
async for解决的是那种“我需要边拿数据边处理,而且拿数据本身是个异步操作”的场景。举个例子,你从数据库分页查询大量记录,每查一次都要等I/O,总不能等全部数据都查完了再一起处理吧?这时候async for就能派上用场。
它的核心机制依赖于两个特殊方法:__aiter__返回异步迭代器对象,__anext__则返回一个可等待的协程。当async for拿到这个协程后,会自动帮你await它,直到拿到结果再继续循环。
它能做什么?告别“一次性拉取”的笨重方式
async for最擅长处理的场景有几个共同特征:数据源需要分批获取、每批获取之间有I/O等待、处理逻辑可以和获取并行。
拿爬虫来说,我们要爬取某个网站的分页数据。传统做法是写个循环,先请求第一页,处理完,再请求第二页…这样虽然也能工作,但代码里全是显式的await,逻辑散落在各处。用async for可以这样:
asyncdeffetch_pages():page=1whileTrue:data=awaitfetch_page(page)ifnotdata:breakyielddata page+=1asyncdefmain():asyncforpage_datainfetch_pages():process(page_data)看,处理逻辑和获取逻辑分开了,而且通过在生成器里yield,我们实现了懒加载——用多少拿多少,不用一次性把几千页数据全塞进内存。
另一个典型场景是流式处理。比如处理WebSocket传来的实时数据流,或者读取大文件的行。如果文件大到几个G,用with open读内存会炸,用readline同步读又慢。asyncio的官方库里就有aiofiles,支持async for遍历文件行:
asyncwithaiofiles.open('huge_log.txt',mode='r')asf:asyncforlineinf:process(line)每个line的读取都是异步的,文件可能还挂在远程NFS上,但我们的代码看起来跟同步逻辑一样清晰。
怎么用?从自定义异步迭代器说起
使用async for有两种途径:一是直接遍历已经实现异步迭代协议的对象,二是自己写异步生成器。
第一种最省心。很多异步库都内置了支持,比如aiohttp的响应对象可以直接用async for遍历分块内容:
asyncwithsession.get(url)asresponse:asyncforchunkinresponse.content.iter_chunked(1024):write_to_disk(chunk)第二种需要自己动手。如果不用生成器语法,可以手动实现__aiter__和__anext__:
classAsyncCounter:def__init__(self,limit):self.limit=limit self.current=0def__aiter__(self):returnselfasyncdef__anext__(self):ifself.current>=self.limit:raiseStopAsyncIterationawaitasyncio.sleep(0.1)# 模拟异步操作result=self.current self.current+=1returnresult然后在async for里使用它:
asyncfornuminAsyncCounter(5):print(num)不过更常见的方式是用async def配合yield写异步生成器,语法糖帮我们把上面那些样板代码全隐藏了:
asyncdefasync_counter(limit):foriinrange(limit):awaitasyncio.sleep(0.1)yieldi运行效果完全一样。Python的yield在异步函数里会自动把一个普通函数变成异步生成器,迭代时行为等同于实现了__aiter__和__anext__的对象。
最佳实践:别把它当万能钥匙
async for虽然好用,但并不是所有循环场景都适合用。几个经验性的建议:
判断异步等待点在哪里。如果循环体里根本没有异步操作,那用async for纯粹是多此一举,还会引入不必要的上下文切换开销。只有当你确定“获取迭代的下一个元素”这个操作需要等待I/O时,才值得用。
注意停止条件的实现。跟普通迭代器用StopIteration不同,异步迭代器需要抛出StopAsyncIteration来结束循环。手动实现时漏掉这个异常会导致死循环。
控制并发深度。async for本身是串行的——你必须等前一个元素处理完才能获取下一个。如果想并行拉取,需要配合asyncio.as_completed或者asyncio.gather使用。比如爬虫场景,可以开多个异步任务,每个任务各自用async for读取自己的分页流。
内存管理要小心。虽然async for是懒加载的,但如果在循环里把结果全部收集到一个列表里,那跟一次性加载就没区别了。真正的价值在于“边拿边丢”,处理完一个元素就把它的引用释放掉。
和其他迭代方式的对比
Python里有四种迭代方式:普通for、列表推导式、生成器yield from、以及async for。它们之间有交叉,但核心区别在于“当迭代本身涉及等待时,能否不阻塞事件循环”。
普通for循环是最底层的,它在同步世界里工作良好,但遇到await调用就会阻塞整个线程。如果锁死了事件循环,其他协程就别想跑了。
列表推导式本质上是把普通for的代码压缩到一行,行为完全一致,没有异步对应物。虽然Python 3.6+支持在列表推导式里用await表达式,但那只是把await放在每个元素的生成逻辑里,迭代本身还是同步的。
生成器yield from可以链式委托生成器,但它要求被委托的生成器也是同步的。如果想在异步生成器里委托给另一个异步生成器,得用async for嵌套,或者用asyncio.gather之类的工具。
async for的真正强大之处在于它把“等待获取”这件事从循环体抽离到了迭代器协议层面。这让我们能把数据获取和数据处理清晰地分离开,async for只关心“下一个元素什么时候来”,至于怎么处理、处理完要不要继续,那是循环体的事。
话说回来,async for也不是完全没有缺点。错误处理比普通for复杂一些——取消异步迭代器、处理网络异常、超时等问题都需要额外考虑。另外,如果异步生成器里抛异常,需要在__anext__里正确传播,否则async for可能悄无声息地停止。
总体而言,async for是异步编程工具箱里一个锋利的工具,但也要看准场合再用。碰到那些数据源天生就是流式、每批数据获取都需要I/O等待的场景,它就是最好的选择。如果是内存里已经存在的列表或数组,直接用普通for就好,强行加async反而画蛇添足。
