async for 要求对象实现 __aiter__ 和 __anext__,HTTP 响应对象不可直接异步迭代;需自定义 AsyncIterator 子类,在 __aiter__ 中返回 self,在 __anext__ 中按需请求分页、更新游标并逐项 yield,复用 client 并合理配置超时与连接池。

async for 要求对象实现 __aiter__,不是所有“异步返回列表”的东西都支持
直接对 aiohttp.ClientSession.get() 或 httpx.AsyncClient.get() 的响应调用 async for 会报 TypeError: 'ClientResponse' object is not async iterable。因为 HTTP 响应对象本身不提供异步迭代接口,它只是个一次性流载体。真正要遍历的是“分页数据集合”,这个集合得你自己封装成异步可迭代对象。
手动构造 AsyncIterator 子类来包装分页逻辑
核心是写一个继承 collections.abc.AsyncIterator 的类,在 __anext__ 中控制请求下一页、解析 JSON、维护游标或页码。常见错误是把整个分页请求写在 __aiter__ 里一次性拉完再 yield,这失去了流式处理意义,也违背 async for 的设计初衷。
关键点:
-
__aiter__只 return self,不做实际请求 -
__anext__每次被调用时发一次请求,解析出当前页的 items,逐个 yield;若无更多数据,抛StopAsyncIteration - 避免在
__annext__中用await asyncio.sleep()硬控频次——应交由上游限流策略(如aiolimiter)处理 - 如果 API 用 cursor 而非 page=1/2/3,记得把上一页返回的
next_cursor存为实例变量,别丢
示例骨架:
立即学习“Python免费学习笔记(深入)”;
class AsyncPageIterator:
def __init__(self, base_url: str, session: aiohttp.ClientSession):
self.base_url = base_url
self.session = session
self.cursor = None # 初始无 cursor
<pre class="brush:php;toolbar:false;">def __aiter__(self):
return self
async def __anext__(self):
params = {}
if self.cursor:
params["cursor"] = self.cursor
async with self.session.get(self.base_url, params=params) as resp:
data = await resp.json()
items = data.get("items", [])
if not items:
raise StopAsyncIteration
self.cursor = data.get("next_cursor") # 更新游标
return items[0] # 每次只 yield 一个 item,保持流式用 async for 遍历时,别在循环体里做阻塞或同步 I/O
常见翻车点:在 async for item in AsyncPageIterator(...) 循环内部调用 time.sleep()、open()、json.dump() 或其他同步文件/计算操作。这会阻塞整个 event loop,让后续分页请求卡住。
正确做法:
- 写文件用
asyncio.to_thread(open, ..., "w")(Python 3.9+)或aiofiles - 批量处理 item 时,用
asyncio.gather(*[process(item) for item in batch]),别用 for + await - 如果某次处理失败需跳过,直接
continue,不要 try-except 吞掉StopAsyncIteration
注意 aiohttp 和 httpx 对连接复用和超时的默认差异
同一个 AsyncPageIterator 实例必须复用同一个 aiohttp.ClientSession 或 httpx.AsyncClient,否则每页新建 client 会触发 DNS 查询+TCP 握手+TLS 协商,性能断崖下跌。但很多人忽略 client 的 timeout 设置:
-
aiohttp默认timeout=5s,对慢接口容易中断;建议显式传aiohttp.ClientTimeout(total=30) -
httpx默认无总超时(仅 connect/read 分开设),但若服务端分页响应慢,可能卡死在某一页 - 两者默认都不启用 connection pool 复用?错——都默认开启,但 pool size 默认是 10,高并发分页时可能不够,可调大
connector = aiohttp.TCPConnector(limit=100)
漏掉这些,async for 表面跑着,实际吞吐量还不如同步 requests + time.sleep。
分页接口的异步迭代本质是状态机驱动的协程调度,不是语法糖。游标管理、错误重试、连接生命周期、流控边界——这些没理清,async for 写出来也只是个好看但不可靠的壳。


















