DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Python 调用异步函数没有执行,提示 coroutine was never awaited

直接调用 async def 函数只创建协程对象。按当前入口选择 asyncio.run 或 await,需要任务时保留引用并等待结果,避免用忽略警告代替修复。

异步函数里写了操作,调用之后却没有发生,程序还提示 RuntimeWarning: coroutine ... was never awaited:

import asyncio

async def prepare():
    await asyncio.sleep(0)
    return "ready"

prepare()

调用 async def 定义的函数会创建协程对象,单独这一行不会安排它执行。警告提示的是协程没有被等待或交给任务执行,不是函数已经执行完但没有返回值。本文针对 Python 3.7 及以上的标准 asyncio;第三方框架的入口还需遵守其事件循环约定。

普通脚本从 asyncio.run 进入

如果是独立脚本,当前线程没有运行中的事件循环,可以在程序入口运行顶层协程:

import asyncio

async def prepare():
    await asyncio.sleep(0)
    return "ready"

async def main():
    result = await prepare()
    print(result)

if __name__ == "__main__":
    asyncio.run(main())

这段示例预期打印 ready。asyncio.run 管理入口所需的事件循环,在协程完成后清理并关闭它,通常应作为整个程序的入口使用一次。

传给入口的是 main() 创建的协程对象,而不是函数名 main。函数内部等待子协程时使用 await prepare();只创建对象、保存到变量里,也不会自动启动它。

已经处于异步代码中,直接 await

如果调用发生在异步处理函数里,沿用已有事件循环:

async def handle_request():
    result = await prepare()
    return result

这个片段沿用前面的 prepare,由现有框架负责调用 handle_request。同一线程已经运行事件循环时,再调用 asyncio.run 会报错。不能在每个异步函数外面都包一层 run。

若加入 await 后出现“只能在异步函数内使用”的语法问题,应向上检查调用链和入口。单纯把函数声明改成 async def 会改变它的调用方式,上层也必须等待或调度它,不能只改声明就结束。

有些交互环境支持顶层 await,有些普通脚本不支持。判断依据是当前执行环境如何管理协程,而不是代码文件中有没有导入 asyncio。

确实需要并发时,保留任务并等待完成

已有运行中的事件循环,且希望让协程作为任务调度时,可以创建任务,再明确等待它:

async def main():
    task = asyncio.create_task(prepare())
    # 这里可以安排其他独立的异步工作
    result = await task
    print(result)

这段 main 可替换独立脚本中的同名函数。任务对象保存在变量里,结果与异常通过 await task 交回调用方。若只是马上等待一个操作,直接 await prepare() 更简单。

不要只创建后台任务就退出入口,也不要丢掉所有任务引用。事件循环只保留任务的弱引用;应用需要管理任务存活、完成和异常。还需检查最终结果,才能确认业务操作成功。

修复后核对警告中指向的具体协程:顶层调用修好后,内部遗漏的异步调用仍可能触发同类警告。确认所有需要完成的操作都在入口返回之前完成,失败能传递给调用方;不要通过过滤 RuntimeWarning 来当作修复。

涉及发送、写入等操作时,先确认此前是否有部分步骤已执行,再决定是否重试,避免重复副作用。回退异步调用链时保留入口和调用约定,不要混用旧的同步调用方式与新的协程返回值。