异步生成器
生成器有几个函数,平时也许用不到,但有些框架或其他包可能会用到。
| 异步生成器 | 同步生成器 | 作用 |
|---|---|---|
await agen.__anext__() |
next(gen) / gen.__next__() |
推进 |
await agen.asend(x) |
gen.send(x) |
推进并传值进去 |
await agen.athrow(exc) |
gen.throw(exc) |
注入异常 |
await agen.aclose() |
gen.close() |
关闭(抛 GeneratorExit) |
StopAsyncIteration |
StopIteration |
结束信号 |
有的框架或包会得用生成器的特点(含有yield 语句),将其当成资源管理器来使用(with/async with),只有一个yield语句,用于返回resoure handle,yield后面的语句用于释放资源。
下面是一段示例代码,说明了调用方取到了资源,最终能否正常释放资源。
import asyncio
# 实验1:你的生成器 —— yield 后没有 try
async def gen_no_try():
yield 1
print(" [gen_no_try] close连接(yield后的代码)")
# 实验2:yield 用 try 包住 —— 就能"看见"抛进来的 GeneratorExit
async def gen_with_try():
try:
yield 1
except BaseException as e:
print(f" [gen_with_try] 生成器内部接到: {repr(e)}")
# raise # 重新抛出,让 aclose调用方不会抛异常,即GeneratorExit异常会被aclose吞末
raise Exception("别的异常") # 其它异常不会被aclose吞末,正常向外抛
finally:
print(" [gen_with_try] finally 清理")
async def main():
print("实验1:你的写法(yield 后无 try)")
agen = gen_no_try()
await agen.__anext__()
try:
await agen.aclose()
except BaseException as e:
print(f" 消费方接到异常: {repr(e)}")
else:
print(" 消费方:aclose 没抛任何异常")
print("\n实验2:yield 被 try 包住(证明 GeneratorExit 确实被抛进生成器)")
agen2 = gen_with_try()
await agen2.__anext__()
try:
await agen2.aclose()
except BaseException as e:
print(f" 消费方接到异常: {repr(e)}")
else:
print(" 消费方:aclose 没抛任何异常")
asyncio.run(main())
输出结果: 实验1 没有执行yiled后边的代码,所有实际中资源不会被释放
实验1:你的写法(yield 后无 try)
消费方:aclose 没抛任何异常
实验2:yield 被 try 包住(证明 GeneratorExit 确实被抛进生成器)
[gen_with_try] 生成器内部接到: GeneratorExit()
[gen_with_try] finally 清理
消费方接到异常: Exception('别的异常')
生成器使用__anext__向下推进时,他会返回yield后面那个值,但是不代表yield那行语句完成了,其实代码仍然停留在这一行。
aclose函数会将一个GeneratorExit异常抛到yield 1 那行,所以如果yield语句被try包裹住,就可以在生成器函数内部捕获GeneratorExit异常。aclose函数也会吞末GeneratorExit异常,不会将其抛给alcose函数的调用方。但如果生成器内部抛出了GeneratorExit以外的其他类型异常,则aclose函数不会吞末它,而是正常向调用方抛出。
GeneratorExit 一般会做为一个正常关闭生成器的信号而不是异常,所以不会向调用方抛出。 比如一个循环在一半的时候break了,是个正常逻辑,而不是说生成器在那个点之后的代码一定要被处理。
我们将调用方代码改成athrow():
async def main():
print("实验1:你的写法(yield 后无 try)")
agen = gen_no_try()
await agen.__anext__()
try:
await agen.athrow(Exception("业务异常中断迭代器"))
except BaseException as e:
print(f" 消费方接到异常: {repr(e)}")
else:
print(" 消费方:athrow 没抛任何异常")
print("\n实验2:yield 被 try 包住(证明 athrow主动将一个异常注入进生成器)")
agen2 = gen_with_try()
await agen2.__anext__()
try:
await agen2.athrow(Exception("业务异常中断迭代器"))
except BaseException as e:
print(f" 消费方接到异常: {repr(e)}")
else:
print(" 消费方:athrow 没抛任何异常")
asyncio.run(main())
运行结果:
实验1:你的写法(yield 后无 try)
消费方接到异常: Exception('业务异常中断迭代器')
实验2:yield 被 try 包住(证明 athrow 确实主动将一个异常注入进生成器)
[gen_with_try] 生成器内部接到: Exception('业务异常中断迭代器')
[gen_with_try] finally 清理
消费方接到异常: Exception('别的异常')
athrow会主动将一个异常传到生成器内部yield 1那行。
下面的代码演示正常情况下是通过继续推进迭代器来结束生成器,并释放资源的(当做为资源管理器时)
async def main():
print("实验1:你的写法(yield 后无 try)")
agen = gen_no_try()
await agen.__anext__()
try:
await agen.__anext__()
except StopAsyncIteration as e:
print(f" 消费方接到异常: {repr(e)}")
else:
print(" 消费方没抛任何异常")
print("\n实验2:yield 被 try 包住")
agen2 = gen_with_try()
await agen2.__anext__()
try:
await agen2.__anext__()
except StopAsyncIteration as e:
print(f" 消费方接到异常: {repr(e)}")
else:
print(" 消费方:athrow 没抛任何异常")
asyncio.run(main())
输出结果:
实验1:你的写法(yield 后无 try)
[gen_no_try] close连接(yield后的代码)
消费方接到异常: StopAsyncIteration()
实验2:yield 被 try 包住
[gen_with_try] finally 清理
消费方接到异常: StopAsyncIteration()
用一个比较贴进现实的场景来说明一下三种结束生成器,并能正常释放资源的方式。
import asyncio
async def get_db():
print(" 连接数据库,开启事务")
try:
yield "db"
print(" ✅ commit(提交事务)") # 只有成功推进(__anext__)才会跑到
except Exception as e:
print(f" ❌ rollback(回滚): {e}") # athrow 注入业务异常时跑
raise
finally:
print(" 关闭连接") # 任何情况都跑(在 finally)
async def drive(name, how):
print(f"[{name}]")
agen = get_db()
await agen.__anext__() # 拿到 db,业务代码开始用
print(" ...业务处理中...")
try:
if how == "success":
await agen.__anext__() # 成功:正常推进
elif how == "error":
await agen.athrow(ValueError("查询失败")) # 失败:把业务异常注入
elif how == "cancel":
await agen.aclose() # 取消:强制关闭
except StopAsyncIteration:
print(" (收到 StopAsyncIteration,正常结束)")
except Exception as e:
print(f" (异常冒回消费方: {e!r})")
print()
async def main():
await drive("成功路径 → __anext__", "success")
await drive("出错路径 → athrow", "error")
await drive("取消路径 → aclose", "cancel")
asyncio.run(main())
运行结果:
[成功路径 → __anext__]
连接数据库,开启事务
...业务处理中...
✅ commit(提交事务)
关闭连接
(收到 StopAsyncIteration,正常结束)
[出错路径 → athrow]
连接数据库,开启事务
...业务处理中...
❌ rollback(回滚): 查询失败
关闭连接
(异常冒回消费方: ValueError('查询失败'))
[取消路径 → aclose]
连接数据库,开启事务
...业务处理中...
关闭连接
看这三条路径,同一个 get_db,三个方法唤醒出三种不同的清理行为——这就是它们各自的专属场景:
| 唤醒方式 | 对应真实场景 | 生成器里跑了哪段 |
|---|---|---|
__anext__() |
请求成功处理完 | yield 后正常代码(commit)+ finally(close) |
athrow(exc) |
请求处理中出错了 | except(rollback)+ finally(close),异常继续上报 |
aclose() |
请求被取消(客户端断开/超时/提前 break) | 只有 finally(close);commit 和 rollback 都跳过 |
三者为什么不能互换
- 成功用 anext:因为你要让 yield 之后的"成功专属逻辑"(commit)执行。换成 aclose 就会跳过 commit,事务白做了。
- 出错用 athrow:因为你要把"出了什么错"告诉依赖,让它走 except 做出错专属清理(rollback)。注意 athrow 传的是真实业务异常,依赖能据此区别对待。
- 取消用 aclose:此时既不算成功也不算业务出错,你只想"无条件关掉"。注意结果——commit 没跑(没成功),rollback 也没跑(GeneratorExit 不是 Exception,except Exception 抓不到它),只有 finally 的 close 执行。这正是"取消时至少保证资源释放"的语义
谁在真实框架里调这三个?
你几乎不会手写这三个调用。它们被封装在:
- @asynccontextmanager 的 aexit:with 块正常结束 → 内部 anext 走成功路径;with 块抛异常 → 内部 athrow(那个异常) 走出错路径。async with 帮你自动选。
- AsyncExitStack.aclose():请求结束时逐个 unwind,按上面规则驱动每个依赖。
- aclose 单独的场景:你 async for 提前 break、或生成器被垃圾回收时,Python/框架自动调它兜底。
所以结论:你写依赖时,只管把清理放进 try/except/finally,不同分支对应不同结束场景;由谁在什么时候调哪个方法,交给 async with / AsyncExitStack 去决定。你现在理解了底层三个方法的分工,就明白 async with 到底在背后"自动选路"选了什么。

浙公网安备 33010602011771号