异步生成器

生成器有几个函数,平时也许用不到,但有些框架或其他包可能会用到。

异步生成器 同步生成器 作用
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);commitrollback 都跳过

三者为什么不能互换

  • 成功用 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 到底在背后"自动选路"选了什么。

posted @ 2026-07-03 12:10  RolandHe  阅读(2)  评论(0)    收藏  举报