Python编程Python开发者

是什么阻止了**Python**协程对象在完成等待后重新启动,而生成器函数则在每次调用时实例化新的迭代器?

用 Hintsage AI 助手通过面试

问题的答案

Python中,通过async def创建的协程被实现为单次使用的状态机,这些状态机在CPython字节码级别由CO_ITERABLE_COROUTINE或原生协程标志管理。当你调用一个异步函数时,它立即返回一个包含帧对象和执行状态的协程对象;等待它会将这个状态机驱动到完成,此时内部的f_lasti(最后指令)标记将达到结束,帧被标记为耗尽。Python运行时显式地通过检查这个完成标志来防止重新进入,如果发生后续的等待,就会引发RuntimeError,因为协程被设计为表示单个、离散的异步操作,具有线性的控制流。相反,生成器函数是工厂——每次调用创建一个全新的PyGenObject,它具有独立的堆栈帧和指令指针,允许该函数生成多个独立的迭代器,每个迭代器维护独立的执行上下文。

生活中的情况

一个开发团队正在构建一个具有弹性的WebSocket客户端,需要使用指数退避重试失败的连接尝试。他们最初在模块级别定义了一个连接协程,并试图在重试逻辑中重用它。

import asyncio async def establish_connection(): return await websockets.connect("wss://api.example.com") # 模块级别实例化 connection_coro = establish_connection() async def retry_connect(max_attempts=3): for attempt in range(max_attempts): try: ws = await connection_coro # 在第二次迭代时失败 return ws except Exception: await asyncio.sleep(2 ** attempt)

当第二次循环迭代尝试再次等待connection_coro时,问题出现了,这导致了RuntimeError,因为第一次成功的等待已经耗尽了协程对象。团队考虑了三种架构解决方案。

一种方法是在捕获到RuntimeError后,在异常块内手动重建协程对象。虽然在技术上可行,但这引入了脆弱的状态管理,并使代码依赖于通过异常处理来检测耗尽,这在语义上是模糊的,并可能掩盖连接逻辑内部的合法运行时错误。

另一个解决方案建议将establish_connection转换为实现__await__的类以创建一个可重置的可等待对象。这提供了一种工厂模式,但增加了不必要的样板代码和复杂性,掩盖了建立连接的简单意图,并需要手动状态跟踪,这重复了Python运行时通过函数调用提供的功能。

所选择的解决方案是将异步函数视为工厂,通过将调用位置移入循环中,确保每次迭代都接收到一个全新的协程对象。通过重构为ws = await establish_connection(),每次尝试实例化一个新的状态机,具有独立的资源管理。这与Python的设计哲学一致,即异步函数是一次性计算期货的构造函数,产生了干净的、无异常的重试逻辑,正确地将失败的连接尝试与后续的重试隔离开。

候选人常常忽略的

为什么将协程存储在变量中并忘记等待会产生资源泄漏,以及close()如何缓解这个问题?

候选人通常认为未等待的协程仅仅进行垃圾回收而没有副作用。然而,如果一个协程已进入其主体并在一个await表达式处暂停(例如,持有一个数据库连接或锁),帧将保留对这些资源的引用。对协程对象调用close()会强制通过帧触发GeneratorExit异常,立即触发上下文管理器(async with)和try/finally块释放资源。没有显式的close(),这些资源将保持占用,直到循环垃圾回收运行,这在连接池耗尽的场景中可能为时已晚。

inspect.iscoroutine()inspect.isawaitable()有什么不同,为什么当编写通用asyncio工具时,这一区别很重要?

inspect.iscoroutine()仅对由async def函数创建的原生协程对象返回True,而inspect.isawaitable()对任何实现了__await__的对象返回True,包括协程、任务、期货和自定义可等待对象。候选人忽视了asyncio函数如ensure_future()接受任何可等待对象,而不仅仅是协程。严格检查iscoroutine()的库会拒绝诸如asyncio.Queue().get()或自定义的未来对象等有效的可等待对象,从而破坏了通用工具函数中调度任意异步操作的多态性。

async forawait在消费异步生成器时有什么区别,为什么前者要求__aiter__返回生成器本身而不是协程?

await消费一个协程或未来并返回单个值,而async for迭代一个异步迭代器,在每个async def生成器函数中的yield处暂停。候选人混淆了async for与等待一个协程列表。至关重要的是,__aiter__必须直接返回异步迭代器对象(而不是可等待对象),因为Python运行时同步调用__aiter__以获取迭代器,然后开始迭代协议。从__aiter__返回协程将导致TypeError,因为协议期望立即访问迭代器的__anext__方法以驱动异步迭代状态机。