异步生成器需用 async def + async yield,且内部只能混用 await 和 yield,不能含同步阻塞调用;返回 async_generator,须用 async for 遍历,I/O 操作需替换为异步版本(如 aiofiles),CPU 密集型任务应交由 run_in_executor 处理。

async def 里不能直接用 yield
Python 的异步生成器不是给普通 yield 加个 async 就能跑通的。如果你写了 async def gen(): yield 1,解释器会直接报错:SyntaxError: 'yield' inside async function。这是因为同步 yield 返回的是 Iterator,而异步生成器必须返回 AsyncIterator,底层协议完全不同。
真正要做的,是把 yield 换成 yield 的异步等价物——也就是 async for 可消费的 async yield,但 Python 语法里它就叫 yield,只是必须出现在 async def 函数中,且函数体里**只能混用 await 和 yield,不能出现同步阻塞调用**。
- 错误写法:
async def bad(): yield time.sleep(1); yield 42(time.sleep是同步阻塞) - 正确前提:所有耗时操作必须是
awaitable,比如await asyncio.sleep(1)或await aiohttp.get(...) - 异步生成器函数返回的对象类型是
async_generator,不是generator
用 async yield 替换 yield,并确保内部调用可 await
重构的核心动作就两步:改函数声明 + 改 yield 表达式前的计算逻辑。原来同步生成器里每一段“准备数据”的代码,如果涉及 I/O、网络、文件读取等,就得拆出来用 await 调用异步版本。
例如,原同步生成器逐行读文件:
立即学习“Python免费学习笔记(深入)”;
def read_lines_sync(path):
with open(path) as f:
for line in f:
yield line.strip()
重构为异步版本时,不能继续用 open(),得换成支持异步 I/O 的方式(如 aiofiles):
import aiofiles
async def read_lines_async(path):
async with aiofiles.open(path) as f:
async for line in f:
yield line.strip()
- 注意
async for是专门用于遍历AsyncIterator的语法,不能用在同步生成器上 -
aiofiles.open返回的是异步上下文管理器,必须用async with - 如果原逻辑里有 CPU 密集型处理(如解析大 JSON),异步生成器不会自动提速,反而可能因事件循环调度变慢;这种场景更适合
loop.run_in_executor
调用方必须用 async for 或手动调用 __anext__
异步生成器不能像同步生成器那样用 for x in gen() 直接遍历,否则会报 TypeError: 'async_generator' object is not iterable。消费者代码也得同步升级。
- 推荐方式:
async for item in read_lines_async("data.txt"): - 手动控制(少见但有用):
agen = read_lines_async("data.txt"); item = await agen.__anext__() - 想转成列表?不能用
list(agen),得用:items = [item async for item in agen] - 若需兼容同步/异步两种调用路径,不要试图“自动检测”,应明确拆分为两个函数,避免隐藏状态和调试困难
别忽略异常传播和资源清理的差异
异步生成器的 async with 和 async finally 行为与同步版本不完全一致。比如你在 async def 里开了数据库连接,又在 yield 后抛了异常,那么 async finally 块里的清理逻辑是否执行,取决于消费者怎么结束迭代。
- 正常消费完(
async for自然结束)→async finally会执行 - 消费者中途
break或抛异常 → 会触发aclose(),但具体时机依赖事件循环调度,不如同步finally确定 - 建议:关键资源(如连接、锁)尽量包裹在
async with内部,而不是靠try/async finally手动管理 - 测试时务必覆盖中断场景,比如用
agen.aclose()主动关闭,观察资源是否释放
最常被跳过的点是:以为把 def 换成 async def、yield 留着不动就完了——其实 yield 前的每一步都得重新评估是否线程安全、是否可 await、是否该进 executor。异步不是魔法,只是把阻塞点显式暴露出来了。


















