Python的异步编程把我搞崩溃了,原来await加不加完全是两种人生

简介: 本文以真实爬虫bug为例,深入剖析Python中`await`的关键作用:不加`await`仅返回协程对象,函数体根本不执行;加上`await`才真正调度并等待其完成。厘清`await`与`asyncio.gather`区别,总结常见翻车场景及避坑指南。(239字)

讲个真事儿。

去年我写了一个异步爬虫,要抓取几千个网页。代码大概长这样:

import aiohttp
import asyncio

async def fetch(session, url):
   async with session.get(url) as response:
       return await response.text()

async def main():
   urls = [...]  # 几千个网址
   async with aiohttp.ClientSession() as session:
       for url in urls:
           html = fetch(session, url)   # 注意这里没加 await
           print(len(html))

跑起来之后,程序瞬间打印了几千行 len(html),每行都是同一个数字。我一开始还以为网速飞快,结果仔细一看,所有输出都是 0 或者一个奇怪的小数字。更诡异的是,服务器端根本没有收到任何请求。

我盯着代码看了十分钟,才发现问题:fetch(session, url) 是一个异步函数,调用它只会返回一个协程对象,不会真正执行。我把它赋值给 html,然后 len(html) 自然就报错了——但为什么没报错?因为协程对象居然有 __len__ 吗?不,其实 len() 会直接抛 TypeError。但我当时用的是 print(len(html)),而 html 是一个协程对象,len() 确实会报错。可为什么我没看到报错?因为我用了 try/except 包住了整个循环,把所有异常都吞掉了。

except 去掉之后,满屏的 TypeError: object of type 'coroutine' has no len()。那一刻我才真正理解:在 async 函数里调用另一个 async 函数,不加 await,就等于买了一张电影票但没去电影院——票是有了,电影没放。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (84).png

async/await 到底在干什么

要理解 await 加与不加的区别,得先明白 Python 异步编程的基本模型。

当你用 async def 定义一个函数时,它就不再是一个普通函数了,而是一个协程函数。调用它不会执行函数体,而是返回一个协程对象。这个协程对象像一个“任务说明书”,里面写着“如果你让我跑,我会做这些事情”。

async def say_hello():
   print("Hello")

# 调用协程函数,返回协程对象,不会打印任何东西
coro = say_hello()
print(coro)  # <coroutine object say_hello at 0x...>

要让协程真正执行,必须把它交给事件循环。最直接的方式就是用 await

async def main():
   await say_hello()  # 现在才会打印 Hello

asyncio.run(main())

await 的作用可以概括为两点:

  1. 执行:告诉事件循环“把这个协程跑起来”。
  2. 等待:挂起当前协程,直到被等待的协程完成,并获取它的返回值。

所以,await 既是“启动开关”,也是“等待信号”。不加 await,协程对象就只是一个对象,永远不会被执行。

不加 await 的几种典型翻车现场

翻车一:以为调用了,其实没执行

async def save_data(data):
   await db.insert(data)
   print("保存成功")

async def main():
   save_data({"name": "张三"})   # 忘记 await
   print("主函数结束")

asyncio.run(main())

输出:

主函数结束

save_data 根本没跑,数据库里什么也没写。更坑的是,Python 不会报错,只会在程序退出时给一个警告:RuntimeWarning: coroutine 'save_data' was never awaited。如果你没开警告,或者警告被日志淹没了,这个 bug 可能几个月都发现不了。

翻车二:把协程对象当结果用

async def get_user(user_id):
   return await db.fetch_one(user_id)

async def main():
   user = get_user(1)   # 没加 await
   print(user.name)     # AttributeError: 'coroutine' object has no attribute 'name'

你拿到的是一个协程对象,不是用户数据。对它做任何属性访问、索引、运算,都会报错。而且错误信息往往让人摸不着头脑。

翻车三:在列表推导里忘记 await

async def main():
   results = [fetch(url) for url in urls]  # 全是协程对象
   for r in results:
       print(r)  # 打印一堆 <coroutine object ...>

正确的写法要么用 await asyncio.gather(*[fetch(url) for url in urls]),要么在循环里逐个 await

翻车四:在非 async 函数里调用 async 函数

def sync_func():
   result = async_func()  # 这里不能加 await,因为 sync_func 不是 async
   return result

await 只能出现在 async def 函数内部。在普通函数里,你只能得到协程对象,要么用 asyncio.run() 跑,要么用 asyncio.create_task() 调度,要么用 loop.run_until_complete()

await 的本质:让出控制权

理解了“不加 await 不执行”之后,还要理解“加了 await 之后发生了什么”。

当你在一个协程里 await 另一个协程时,当前协程会暂停,把控制权交还给事件循环。事件循环可以去执行其他任务。等到被等待的协程完成后,事件循环再把控制权还回来,当前协程从暂停处继续执行。

这就是异步并发的核心:一个线程可以同时处理多个任务,靠的就是在等待 I/O 时主动让出控制权。

async def task_a():
   print("A 开始")
   await asyncio.sleep(1)
   print("A 结束")

async def task_b():
   print("B 开始")
   await asyncio.sleep(1)
   print("B 结束")

async def main():
   await asyncio.gather(task_a(), task_b())

asyncio.run(main())

输出:

A 开始
B 开始
(等待 1 秒)
A 结束
B 结束

两个任务并发执行,总共只花了 1 秒。如果不用 await,或者用同步方式写,就需要 2 秒。

关键点:await 让协程在等待时“让路”,这样事件循环才能去跑别的协程。不加 await,协程根本不会启动,也就没有“让路”这回事。

await 和 asyncio.gather 的区别

很多人分不清 awaitasyncio.gather

  • await coro:等待一个协程完成,并拿到它的返回值。
  • await asyncio.gather(coro1, coro2, ...):并发运行多个协程,等待它们全部完成,返回结果列表。

gather 里面传入的是协程对象,不是 await 后的结果。因为 gather 自己会去调度这些协程。

# 正确:并发执行
results = await asyncio.gather(fetch(url1), fetch(url2))

# 错误:这样会串行执行,而且 fetch 返回的是协程对象,不是结果
results = [await fetch(url1), await fetch(url2)]

第二种写法虽然也能跑,但它是串行的:先等 fetch(url1) 完成,再等 fetch(url2)。失去了并发的意义。

什么时候可以不加 await

有一种情况,你故意不加 await,那就是创建任务

asyncio.create_task(coro) 会把协程包装成一个 Task,立即调度到事件循环中执行,但不会等待它完成。你可以继续做别的事情,稍后再通过 await task 来获取结果。

async def main():
   task1 = asyncio.create_task(fetch(url1))
   task2 = asyncio.create_task(fetch(url2))
   # 这里两个请求已经在后台跑了
   result1 = await task1
   result2 = await task2

这相当于“先点火,后等待”。如果你直接 await fetch(url1),那就变成“点一个火,等它烧完,再点下一个”。

一个完整的对比例子

假设你要从三个 URL 抓取数据,然后合并结果。

错误写法(忘记 await):

async def main():
   results = []
   for url in urls:
       data = fetch(url)      # 没加 await
       results.append(data)   # 追加的是协程对象
   print(results)             # 一堆 <coroutine object>

串行写法(加了 await,但没并发):

async def main():
   results = []
   for url in urls:
       data = await fetch(url)   # 一次等一个
       results.append(data)

总共耗时 = 三个请求时间之和。

并发写法(正确使用 await 和 gather):

async def main():
   tasks = [fetch(url) for url in urls]
   results = await asyncio.gather(*tasks)

总共耗时 = 最慢的那个请求的时间。

为什么 Python 要这样设计

你可能会问:为什么不像 JavaScript 那样,调用 async 函数就自动开始执行?为什么非要加 await

因为 Python 的协程是显式的。调用一个 async 函数只是创建了一个协程对象,并没有把它注册到事件循环里。只有当你 await 它,或者用 create_taskgather 等工具显式调度它时,它才会运行。

这种设计的好处是控制力更强。你可以自由决定什么时候启动任务、什么时候等待、要不要并发。坏处就是容易忘记加 await,导致“代码没跑”这种低级错误。

如何避免踩坑

几个实用的建议:

  1. 开启警告:Python 默认会为未 await 的协程发出 RuntimeWarning。确保你的日志级别能看到这些警告。
  2. 使用 linter:像 pylintflake8-asyncruff 都能检测出未 await 的协程调用。
  3. 类型注解:给协程函数标注返回类型,比如 async def fetch() -> str:,这样类型检查工具能帮你发现错误。
  4. 统一风格:在代码审查时,特别留意 async def 的调用处。看到 async 函数被调用,先问一句:“await 了吗?”
  5. asyncio.run() 作为入口:确保最外层用 asyncio.run(main()),这样事件循环会正常运行,未 await 的协程也会在退出时给出警告。

总结

await 加与不加,区别就是“执行”和“不执行”。

  • 不加 await:调用 async 函数只会返回一个协程对象,函数体不会执行。你拿到的是一个“待办事项”,而不是结果。
  • 加 await:协程被调度执行,当前协程暂停等待,直到它完成并返回结果。

理解这一点,就理解了 Python 异步编程的第一道门槛。记住一句话:async 函数不是用来直接调用的,而是用来 await 的。 下次写异步代码时,看到 async def,先检查调用处有没有 await——如果没有,要么补上,要么用 create_task 显式调度。

我那个爬虫后来改成了 asyncio.gather 并发抓取,几千个网页几分钟就跑完了。而那个忘记加 await 的 bug,也成了我之后每次写异步代码时都会下意识检查的第一件事。

目录
相关文章
|
3月前
|
人工智能 缓存 自然语言处理
阿里云TokenPlan是什么?百炼Token Plan按Credits计费、ai模型支持及省钱技巧(配置指南)
阿里云百炼Token Plan团队版是面向企业/团队的AI大模型订阅服务,以Credits统一计费,支持qwen3.6-plus、GLM-5、Qwen-Image等20+文本与图像模型,兼容Hermes Agent、OpenClaw等主流工具。提供标准(198元/月)、高级(698元)、尊享(1398元)三档坐席,额度分别为2.5万–25万Credits/月,多租户隔离、数据不用于训练,华北2(北京)地域可用。快速体验:https://t.aliyun.com/U/fPVHqY
556 0
|
3月前
|
人工智能 缓存 自然语言处理
阿里云TokenPlan团队版高级坐席698元和尊享坐席1398元/月,如何选择?
阿里云百炼TokenPlan团队版提供标准(198元)、高级(698元)、尊享(1398元)三档坐席,按Credits计费,支持文本与图像多模型,额度从2.5万至25万/月,适配编程及Agent工具,保障数据隐私,不可退款。阿里云百炼官网快速体验:https://t.aliyun.com/U/fPVHqY
|
5月前
|
人工智能 自然语言处理 监控
阿里云JVS Claw是什么?JVS Claw“养虾”功能与优势,免费体验版和最新包年包月优惠
阿里云JVS Claw是“云上养虾神器”,可简化大模型使用流程,支持零门槛创建、管理和优化AI智能体。其核心功能包括一键接入OpenClaw、多端协同操作、提供安全隔离环境及自进化技能体系,满足企业及个人智能化需求。新用户可享7天免费体验,首月39元起,且提供包月包年优惠。此外,用户也可选用轻量云服务器部署OpenClaw,有多个规格的云服务器可供选择。
|
5月前
|
人工智能 API
HappyHorse 1.0已登陆阿里云百炼,HappyHorse视频生成定价720P 0.9元/秒、1080P 1.6元/秒,API服务可通过阿里云百炼直接调用。
阿里云HappyHorse 1.0视频生成模型正式登陆百炼平台,在百炼平台:https://t.aliyun.com/U/fPVHqY 申请使用,HappyHorse支持文生视频、图生视频及音视频联合生成;具备1080P超分、15秒多镜头叙事能力,画面质感、人物真实感与可控性突出。720P/1080P生成单价分别为0.9元/秒、1.6元/秒,API直调可用。
975 1
|
5月前
|
API 内存技术
DeepSeek-V4正式登陆阿里云百炼!价格低至1元/百万Tokens,性能对标顶级闭源模型
DeepSeek-V4正式登陆阿里云百炼!含Pro与Flash双版本,支持100万字超长上下文及思考/非思考双模式。性能媲美顶级闭源模型,在Agent能力、数学推理、世界知识等评测中领先开源界。API价格低至1元/百万Tokens输入,官方体验链接:https://t.aliyun.com/U/fPVHqY
1770 2
DeepSeek-V4正式登陆阿里云百炼!价格低至1元/百万Tokens,性能对标顶级闭源模型
|
5月前
|
存储 人工智能 自然语言处理
OpenClaw长期记忆:优秀管线与玄学效果
"Memory is limited — if you want to remember something, WRITE IT TO A FILE. ‘Mental notes’ don’t survive session restarts. Files do." —— OpenClaw AGENTS.md 默认模板
|
2月前
|
数据采集 存储 前端开发
某乎爬虫进阶:爬取问题+回答+用户信息,构建知识图谱数据源
本文详解如何爬取知乎问题、回答及用户信息构建知识图谱:突破SSR渲染与反爬限制,采用API直调+Cookie认证+UA池+隧道代理方案;设计三表实体模型(问题/回答/用户),支持关系抽取与Neo4j存储,兼顾合规性与实用性。(239字)
223 2
|
2月前
|
数据采集 人工智能 自然语言处理
自动化比价系统:从采集到数据清洗,全链路打通教程
本文详解淘宝/京东/拼多多三平台自动化比价系统全链路:用OpenClaw自然语言采集、站大爷隧道代理防封(24小时成功率98.2%+)、AI智能清洗价格(统一格式、核销优惠、去重校验),自动生成可决策的比价报告与飞书预警,真正实现“采得稳、洗得准、用得上”。
280 1
|
5月前
|
缓存 自然语言处理
阿里百炼Token Plan团队版支持哪些模型,Credits计费机制规则是什么?
阿里云百炼Token Plan团队版支持Qwen3.6-plus、GLM-5等文本模型及Qwen-Image、WAN2.7等图像模型,统一以Credits计费:按模型类型、Token量、思考模式等动态消耗,优先抵扣月度额度,再用共享包,耗尽则暂停服务。
|
6月前
|
人工智能 JavaScript Linux
【龙虾AI🦞进阶指南】:OpenClaw阿里云/本地保姆级部署+百炼API配置+Claude Code集成Helms-AI,开发效率翻倍!
“在OpenClaw里规划好开发任务,还要手动切到Claude Code写代码;Claude Code改完bug,得自己回OpenClaw同步进度”——这是2026年无数开发者的日常痛点。OpenClaw擅长任务规划、信息检索、文档撰写,Claude Code专注代码编写、调试重构,二者单独使用都是效率神器,但“信息孤岛”导致的来回切换,让效率损耗过半。
1564 5

热门文章

最新文章