# 深入理解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复杂一些——取消异步迭代器、处理网络异常、超时等问题都需要额外考虑。另外,# 最近在写一个异步数据流处理模块的时候,被aiter这个词绊了一下。它不是原生的 Python 关键字,也不是标准库里显眼打包的那个对象,更像是藏在__aiter__和__anext__这两个魔法方法背后的协议。想聊清楚aiter,其实想聊的是异步迭代器这整个生态。
先简单说它是什么。在 Python 里,普通迭代器靠__iter__和__next__干活,调用iter()拿到一个迭代器,然后next()一步步推着它往前走。异步迭代器是对应的异步版本,它的核心是__aiter__和__anext__,但__anext__返回的是一个 awaitable 对象,也就是说,获取下一个值本身是一个异步操作,中间可以挂起、等待。aiter()这个内置函数是在 Python 3.10 正式引入的,专门用来调用对象的__aiter__方法,拿到一个异步迭代器对象。在这之前,大家通常直接手动调用obj.__aiter__(),现在有了统一的入口,就像iter()之于普通迭代器一样自然。
那它到底能做什么?想象一下,你要从一个慢速 API 分页拉取大量数据,每次请求要等一会儿,返回一批结果。你希望一边拉取、一边处理,而不是等全部拉完再处理。这时候异步迭代器就很有用:每次await anext(async_iterator)都发出一个网络请求,拿到一页数据,处理完这页,再自动请求下一页。换句话说,它帮你在迭代过程中自然地插入异步等待,既不会阻塞事件循环,也不会浪费 CPU 在空转等待上。
说到使用方式,最直接的自然是实现一个异步迭代器类。定义__aiter__返回 self,定义__anext__,里面写上异步逻辑,比如await asyncio.sleep(1)或await session.get(url),完成后返回下一个值,没有值时抛出StopAsyncIteration。然后你可以用async for value in async_iter:这样的异步 for 循环来消费它,也可以手动调用aiter()拿到迭代器,再手动await anext()一步步走。不过实际项目里,用生成器更常见:在异步函数里写yield,Python 会自动把它变成一个异步可迭代对象,省去手动定义类的繁琐。例如:
asyncdeffetch_pages():page=1whilepage<=max_pages:data=awaitfetch_page_data(page)yielddata page+=1调用时async for data in fetch_pages():就很简洁。
最佳实践方面,有几个点值得注意。首先,异步迭代器通常用于 I/O 密集型的场景,比如网络请求、文件读写、数据库游标。CPU 密集型的计算即使封装成异步迭代器,也无法利用协程的优势,因为真正的计算不会被挂起。其次,异常处理要仔细,async for循环内部如果抛出异常,会中断迭代,确保在循环外部有 try/finally 或 async with 来清理资源,比如关闭请求会话。还有,不要把异步迭代器和普通迭代器混用,比如在普通 for 循环里传入异步迭代器,那会直接报错。如果必须同时处理同步和异步的流,可以考虑用aiofiles或anyio这类库提供统一的抽象层。
说到同类技术对比,最容易混淆的是异步生成器和普通生成器。普通生成器在每次yield时挂起,但挂起的是当前线程,不能做异步等待;异步生成器在每次yield时也挂起,但你可以用await在 yield 之间做真正的异步等待。另一个容易搞混的是异步可迭代对象和异步上下文管理器:前者重点是逐个产生产出值,后者重点是进入和退出时的生命周期管理,比如打开和关闭连接。虽然两者都可以用async with和async for配合,但职责完全不同。还有一个常被拿出来对比的是asyncio.Queue,它的get方法也像是阻塞等待下一个值,但队列更适合在多个生产者/消费者之间传递数据,而迭代器天然适合单线顺序的流式处理。
最后提一个看着不起眼但实际很影响效率的点:anext()内置函数在 Python 3.10 前后行为有点微妙差异,早期版本如果传入的不是异步迭代器,会直接抛出 TypeError,现在版本会更友好地尝试调用__anext__。如果项目需要兼容 3.9 或更早的版本,还是用await async_iterator.__anext__()或await anext(async_iterator, sentinel)带默认值会更稳妥。总之,aiter不是一个花哨的魔法,它让异步迭代器的使用路径跟同步迭代器对齐,减少了学习成本和心智负担,在一个越来越异步的 Python 世界里,这算是一个贴心的设计。如果异步生成器里抛异常,需要在__anext__里正确传播,否则async for可能悄无声息地停止。
总体而言,async for是异步编程工具箱里一个锋利的工具,但也要看准场合再用。碰到那些数据源天生就是流式、每批数据获取都需要I/O等待的场景,它就是最好的选择。如果是内存里已经存在的列表或数组,直接用普通for就好,强行加async反而画蛇添足。