第 2 章:异步程序入口与协程等待
上一章已经跑通了异步学生查询。如果直接执行 result = query_student_async(1) 并打印,得到的却是 <coroutine object ...>,而不是学生字典;程序结束时还可能出现 coroutine was never awaited 警告。
怎样从普通 Python 脚本真正执行这个查询,并取得学生信息?我们先观察直接调用的结果,再用 await 取得返回值,最后整理脚本入口与函数调用关系。
本章只解决异步函数的最小调用规则。多个任务如何同时执行、并发数量控制、超时、异常、取消,以及 HTTP、FastAPI、数据库和 Redis,留到后续章节。
本章学习目标
学完本章后,你需要能做到:
- 知道
async def定义的是异步函数。 - 理解调用异步函数后得到的是协程对象,不是最终结果。
- 能用
await等待一个异步查询函数执行完成。 - 能从异步函数的
return中拿到结果。 - 能写出
async def main()作为异步入口函数。 - 能用
asyncio.run(main())在普通 Python 脚本中运行异步程序。 - 能排查
coroutine was never awaited、await outside async function、asyncio.run() cannot be called from a running event loop等常见错误。
学生信息查询样例
我们继续使用学生信息查询场景。本章换用小明、小红、小刚这组独立的内存样例,与上一章的张三、李四、王五不是同一批数据;数据内容的变化与异步调用方式无关。
student_data = {
1: {"id": 1, "name": "小明", "age": 13},
2: {"id": 2, "name": "小红", "age": 14},
3: {"id": 3, "name": "小刚", "age": 13},
}查询函数用 await asyncio.sleep(1) 模拟等待数据库、Redis 或外部接口返回结果。上一章已经比较过同步和异步的耗时;这里关注函数怎么调用、结果怎么取得。
异步函数的调用与脚本入口
每一步的完整代码都直接显示在页面中。将对应文件保存到本地 _code_async/c02_asyncio_run_and_await/,然后在 _code_async 目录执行紧跟代码的命令。
1. 异步函数与协程对象
async def 定义异步函数,让函数能够在异步等待时让出执行权。它常用于异步数据库、Redis、HTTP 查询,也会出现在 FastAPI 或 Starlette 的异步接口中。
普通函数调用后会执行函数体并返回结果;调用异步函数时,Python 先创建一个协程对象。可以把它理解为“准备执行的异步过程”,它还不是学生字典。协程对象需要被等待,函数体里的 await asyncio.sleep(1) 才会开始执行。如果只创建它却不等待,常见提示是 coroutine was never awaited。
先故意不等待查询函数,运行后观察返回值与警告。
功能分析:
- 准备一份
student_data内存数据。 - 定义一个
async def query_student_async(student_id)。 - 在普通代码中直接调用它。
- 打印返回值。
文件名:01_coroutine.py
import asyncio
student_data = {
1: {"id": 1, "name": "小明", "age": 13},
2: {"id": 2, "name": "小红", "age": 14},
3: {"id": 3, "name": "小刚", "age": 13},
}
async def query_student_async(student_id):
await asyncio.sleep(1)
return student_data.get(student_id)
result = query_student_async(1)
print(result)运行命令:
uv run python c02_asyncio_run_and_await/01_coroutine.py可能看到的输出:
<coroutine object query_student_async at 0x...>
RuntimeWarning: coroutine 'query_student_async' was never awaited运行逻辑说明:
query_student_async(1)调用了一个异步函数。- 这个调用不会直接返回学生字典。
- 它返回的是一个协程对象。
- 因为这个协程对象没有被
await,所以 Python 可能提示coroutine was never awaited。 - 函数体里的
await asyncio.sleep(1)和return student_data.get(student_id)并没有按你预期执行完成。
注意:这里的代码是故意写错,用来观察现象。
验收时看两点:打印出的是 <coroutine object ...>,而不是学生字典;函数体没有执行,因此不会等待 1 秒。未等待警告通常写到标准错误,出现位置和协程对象的内存地址可能因运行环境而变。
普通函数是“调用就执行”。
异步函数是“调用后得到协程对象,等待后才执行完成”。
2. await 与查询返回值
await 等待异步操作完成,并取得函数的 return 值。它通常写在 async def 函数内部,不能直接写在普通 def 函数或普通 .py 文件顶层。
因此,我们先用 async def main() 放置查询流程,再从普通脚本调用 asyncio.run(main())。main() 只是常见的入口函数名;asyncio.run() 负责创建并管理运行异步代码所需的事件循环。日常脚本里用它启动最外层异步流程,在已经运行的异步函数中则直接用 await。
这次把查询放进 main(),运行后观察 result 的值。
功能分析:
- 继续保留
student_data。 - 继续使用
query_student_async(student_id)。 - 新增
async def main()。 - 在
main()里用await等待查询结果。 - 用
asyncio.run(main())启动异步程序。
文件名:02_await_result.py
import asyncio
student_data = {
1: {"id": 1, "name": "小明", "age": 13},
2: {"id": 2, "name": "小红", "age": 14},
3: {"id": 3, "name": "小刚", "age": 13},
}
async def query_student_async(student_id):
await asyncio.sleep(1)
return student_data.get(student_id)
async def main():
result = await query_student_async(1)
print(result)
asyncio.run(main())运行命令:
uv run python c02_asyncio_run_and_await/02_await_result.py可能看到的输出:
{'id': 1, 'name': '小明', 'age': 13}运行逻辑说明:
- 程序先执行
asyncio.run(main())。 asyncio.run()启动最外层异步函数main()。main()里执行到await query_student_async(1)。query_student_async(1)开始异步执行。await asyncio.sleep(1)模拟异步等待 1 秒。- 等待结束后,函数返回
student_data.get(student_id)。 result拿到返回的学生字典。print(result)打印查询结果。
注意:
await 后面拿到的是异步函数 return 的值。
这一点很重要。
result = await query_student_async(1)这里的 result 才是学生数据。
如果少了 await:
result = query_student_async(1)这里的 result 只是协程对象。把两次运行连起来看:
3. 脚本入口与模块导入
前一个版本已经能拿到学生字典。接下来把 asyncio.run(main()) 放在 if __name__ == "__main__" 下:只有直接运行当前文件时才启动脚本,其他文件导入它时不会自动执行查询。
查询过程也可以继续拆成异步函数:main() 用 await 调用 print_student(),后者再用 await 查询学生。查询不到时返回 None,打印提示后结束这次处理。
功能分析:
- 查询一个存在的学生 ID。
- 查询一个不存在的学生 ID。
- 如果查询结果是
None,打印友好提示。 - 使用
if __name__ == "__main__": asyncio.run(main())启动程序。
文件名:03_async_entry.py
import asyncio
student_data = {
1: {"id": 1, "name": "小明", "age": 13},
2: {"id": 2, "name": "小红", "age": 14},
3: {"id": 3, "name": "小刚", "age": 13},
}
async def query_student_async(student_id):
await asyncio.sleep(1)
return student_data.get(student_id)
async def print_student(student_id):
result = await query_student_async(student_id)
if result is None:
print(f"没有找到学生:{student_id}")
return
print(f"查询到学生:{result}")
async def main():
await print_student(1)
await print_student(99)
if __name__ == "__main__":
asyncio.run(main())运行命令:
uv run python c02_asyncio_run_and_await/03_async_entry.py可能看到的输出:
查询到学生:{'id': 1, 'name': '小明', 'age': 13}
没有找到学生:99运行逻辑说明:
- 程序从文件底部开始判断
if __name__ == "__main__"。 - 当前文件被直接运行时,这个条件成立。
- 程序执行
asyncio.run(main())。 main()作为异步主流程开始运行。await print_student(1)查询存在的学生。print_student()内部继续await query_student_async(student_id)。- 查询到数据后,打印学生信息。
await print_student(99)查询不存在的学生。- 查询结果是
None,所以打印“没有找到学生”。
注意:
main() 只是一个常见名字,不是 Python 强制要求。
但在脚本里使用 main() 可以让程序结构更清楚:
普通入口 -> asyncio.run(main()) -> async main() -> await 具体异步函数if __name__ == "__main__" 的够用理解是:
只有这个文件被直接运行时,才执行下面的启动代码。
这个写法适合脚本,也方便后续把函数拿到其他文件里复用。
调用规则与错误排查
普通脚本的调用规则
- 在
async def函数里用await等待异步查询并取得返回值。 - 用
async def main()组织脚本的异步主流程。 - 在普通脚本入口用
asyncio.run(main())启动,不要在已运行的异步函数里再次调用。
常见调用错误
| 看到的现象 | 原因 | 调整方式 |
|---|---|---|
coroutine was never awaited | 创建了协程对象,但没有等待它;演示一故意这样写。 | 在异步函数中 await,或通过脚本入口启动异步流程。 |
SyntaxError: 'await' outside async function(顶层写法可能提示 outside function) | 把 await 写在普通 def 函数或普通脚本顶层。 | 把等待逻辑放进 async def main(),再用 asyncio.run(main()) 启动。 |
asyncio.run() cannot be called from a running event loop | 已在运行的异步流程里再次调用 asyncio.run()。 | 异步函数之间直接使用 await;只在普通脚本入口使用 asyncio.run()。 |
本章小结
本章只讲了一件事:异步函数怎么正确运行。
你需要记住 4 个结论:
async def定义的是异步函数。- 调用异步函数后,先得到协程对象,不是最终结果。
- 想拿到异步函数的返回值,要在异步函数内部使用
await。 - 普通 Python 脚本通常使用
asyncio.run(main())启动异步程序。
从本章开始,后续代码会经常使用这个结构。下面是沿用前文 asyncio 导入和 query_student_async() 定义的入口片段:
async def main():
result = await query_student_async(1)
print(result)
if __name__ == "__main__":
asyncio.run(main())把这个结构记熟,后面学习多个任务、超时、HTTP 请求和 FastAPI 时会轻松很多。
练习
练习 1:先直接执行 result = query_student_async(1),打印结果。检查是否只看到协程对象、没有等 1 秒,也没有打印学生字典;未等待警告可能随后出现。
练习 2:把查询逻辑放进 async def main(),再用 await 拿结果。检查是否等约 1 秒后打印学生字典,且不再出现未等待警告。
练习 3:故意把 await 写到普通 def 函数里,观察 SyntaxError;再放回 async def 函数,并从脚本入口运行,确认能重新拿到学生字典。
练习 4:在 03_async_entry.py 的 main() 内,把 await print_student(1) 故意改成 asyncio.run(print_student(1))。运行后应看到 RuntimeError: asyncio.run() cannot be called from a running event loop;改回 await print_student(1),确认两个查询都恢复正常。报错后还可能出现未等待警告,因为内层 asyncio.run() 没能运行传入的协程。