小白学 Python
AI 编程指南
© 2026 小白学 Python · 基于 walter201230/Python 教程
课程目录关于本站联系方式隐私政策GitHub

全部课程

  1. 00为什么学 Python
  2. 01Python 安装与第一个程序
  3. 02基本数据类型与变量
  4. 03List 与 Tuple
  5. 04Dict 与 Set
  6. 05条件与循环
  7. 06函数
  8. 07迭代
  9. 08面向对象
  10. 09模块
  11. 10魔法方法
  12. 11枚举类
  13. 12元类 metaclass
  14. 13线程与进程
  15. 14正则表达式
  16. 15闭包
  17. 16装饰器
  18. 17代码可读性
  19. 18pathlib:现代化路径处理
  20. 19异常处理
  21. 20dataclass
  22. 21上下文管理器(with)
  23. 22async / await
  24. 23pyproject.toml + uv
  25. 24ruff:一站式代码检查
  26. 25pytest:测试框架
  27. 26日志 logging
  28. 27打包发布到 PyPI
  29. 28学完之后
Python 环境加载中…

async / await

4 道练习题·预计 60 分钟·做对一题解锁下一段

完整通读本章 →

教学 01 / 17· 已读

异步编程:async/await 让一个线程同时等一百件事

各位写过爬虫没有?随手写一段:

Python· 可在网页运行
import time


def fake_get(url):
    time.sleep(0.01)
    return f'GOT {url}'


def main():
    urls = [f'https://example.com/{i}' for i in range(5)]
    start = time.perf_counter()
    results = []
    for u in urls:
        results.append(fake_get(u))
    cost = time.perf_counter() - start
    print(f'拿到 {len(results)} 个,用了 {cost:.3f} 秒')


main()

输出大致是这样:

文本
拿到 5 个,用了 0.05 秒

每个请求假装耗时 0.01 秒,5 个串起来就是 0.05 秒。如果是真实的网络请求,每个 200 毫秒,100 个串起来就是 20 秒,慢得想骂人。

「那好办,开线程嘛!」有童鞋立刻反应过来,「100 个 URL 就开 100 个线程,一起跑。」

线程当然能解决问题。但开 100 个线程是不是有点奢侈?这 100 个线程 99% 的时间都在干同一件事:等网络包回来。本质上这是个等待问题,不是个计算问题。为了「等」而开 100 个操作系统级的线程,CPU 不开心,内存也不开心。

而且线程一多,「锁」、「竞态」、「死锁」这些老朋友就会一个一个找上门。你只是想抓个网页,怎么忽然要去研究操作系统了?

Python 给了我们另一条路:asyncio 。它的卖点很直白——一个线程,同时等一百件事。各位听到这句话第一反应肯定是:「这不科学吧,单线程怎么同时做一百件事?」

这就是这一章要回答的问题。

教学 02 / 17· 已读

直觉对比:串行 5 秒 vs 并发 1 秒

先不讲原理,先看效果。下面这段是「正经的」串行代码,三件事各等 1 秒:

Python· 可在网页运行
import time


def task(name):
    time.sleep(1)
    return f'{name} 完成'


start = time.perf_counter()
r1 = task('打卡')
r2 = task('查询')
r3 = task('上传')
cost = time.perf_counter() - start
print(r1, r2, r3)
print(f'总共 {cost:.2f} 秒')

跑出来:

文本
打卡 完成 查询 完成 上传 完成
总共 3.01 秒

一秒一秒老老实实排队。这没什么好说的。

接下来换成异步版本:

Python· 可在网页运行
import asyncio
import time


async def task(name):
    await asyncio.sleep(1)
    return f'{name} 完成'


async def main():
    start = time.perf_counter()
    r1, r2, r3 = await asyncio.gather(
        task('打卡'),
        task('查询'),
        task('上传'),
    )
    cost = time.perf_counter() - start
    print(r1, r2, r3)
    print(f'总共 {cost:.2f} 秒')


asyncio.run(main())

跑出来:

文本
打卡 完成 查询 完成 上传 完成
总共 1.00 秒

3 秒变 1 秒,三件事居然真的「同时」完成了。各位先别管 async 、 await 、 gather 这些词是什么意思,先记住一个事实:asyncio.sleep(1) 在一个协程里等的时候,另一个协程可以去用 CPU 干自己的活,三个协程的等待是重叠的,所以总时间就是最长那一个,而不是三者之和。

这就是 asyncio 的核心魔法。它没有偷偷开线程,也没有把 CPU 加速。它做的事很朴素:当某个协程在等 IO 的时候,让出 CPU 给其他协程,等回来再继续。

讲到这里,各位心里应该有了点直觉。下面我们一步一步把这套语法拆开来看。

教学 03 / 17· 已读

第一个 async 函数:和普通函数差在哪

普通函数长这样:

Python· 可在网页运行
def hello():
    return '两点水好'


print(hello())

输出:

文本
两点水好

加一个 async 关键字,就成了「协程函数」(coroutine function):

Python· 结合前文阅读
async def hello():
    return '两点水好'


print(hello())

输出:

文本
<coroutine object hello at 0x...>
RuntimeWarning: coroutine 'hello' was never awaited

各位看出区别了吗?同样是 print(hello()) ,普通函数返回的是字符串 '水哥好' ,协程函数返回的却是一个 协程对象——一个 <coroutine object> ,而不是字符串。

更要命的是,Python 还甩了一句警告:「这协程从来没被 await 过」(coroutine was never awaited)。

这是什么意思?关键的认知就在这里:

  • async def 定义的不是一个「会立刻跑的函数」
  • 调用 async def 定义的函数,它根本没跑,只是给你返回一个「待执行的任务清单」

打个比方,普通函数 hello() 像是「派人去执行任务,立刻拿到结果」。而协程函数 hello() 像是「写一张任务单递给你,至于什么时候去做、谁去做,另说」。

这张「任务单」就是协程对象。它必须被 await ,或者扔给事件循环去执行,里面的代码才会真正跑起来。

教学 04 / 17· 已读

await:让协程跑起来

那怎么让这张任务单真的执行?用 await :

Python· 可在网页运行
import asyncio


async def hello():
    return '两点水好'


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


asyncio.run(main())

输出:

文本
两点水好

这下拿到字符串了。await hello() 的语义可以读成「请帮我把 hello() 这张任务单完成,然后把结果给我」。

但这里有个非常重要的限制:await 只能写在 async def 函数体内部。各位试试在普通函数里写 await ,Python 会告诉你 SyntaxError: 'await' outside async function 。

「那我想在普通脚本里调用一个 async def 怎么办?」这就要用到下一个角色——asyncio.run() 。

教学 05 / 17

asyncio.run():进入异步世界的大门

asyncio.run() 是同步代码和异步代码之间的「门」。它接收一个协程对象,启动一个「事件循环」(event loop),把协程跑完,然后关掉事件循环:

Python· 可在网页运行
import asyncio


async def main():
    print('我在异步世界里')
    await asyncio.sleep(0.01)
    print('我又在异步世界里')


asyncio.run(main())

输出:

文本
我在异步世界里
我又在异步世界里

这里隐含了一个非常重要的设计:整个程序里通常只有一个事件循环,由 asyncio.run() 启动。所有的协程都跑在这个循环里。各位可以这么想象:

  • 同步世界 → 一条直直的路,一步接一步
  • 异步世界 → 一个调度中心(事件循环),里面挂着一堆协程,谁能跑就让谁跑

asyncio.run() 就是从同步世界踏进异步世界的入口。一个程序只该调用一次(嵌套调用会报错)。

到此为止,各位掌握了三个基础概念:async def 定义协程函数、 await 等协程结果、 asyncio.run() 启动事件循环。这三个东西凑齐了,你已经能写一个最小可跑的异步程序。

练习 1 / 4·第一个 async 函数题目有问题?

请用 async def 定义一个协程函数 hello,函数体打印 'hi'。

然后用 asyncio.run(hello()) 启动它。

输出应该是:

文本
hi
main.py
可编辑
🔒做对当前题解锁下一段 ·0/4

推荐一个,全网最全最友好的中文 AI 编程指南: coding.stormzhang.ai

← 上一章21 · 上下文管理器(with)
async / await
下一章 →23 · pyproject.toml + uv
本章目录

async / await

  1. 教学 01异步编程:async/await 让一个线程同时等一百件事
  2. 教学 02直觉对比:串行 5 秒 vs 并发 1 秒
  3. 教学 03第一个 async 函数:和普通函数差在哪
  4. 教学 04await:让协程跑起来
  5. 教学 05asyncio.run():进入异步世界的大门
  6. 练习 1第一个 async 函数
  7. 练习 2 🔒串行 await 的顺序
  8. 教学 06 🔒协程对象不 await 会怎样
  9. 教学 07 🔒一次等多个:asyncio.gather
  10. 练习 3 🔒用 gather 并发跑两个协程
  11. 练习 4 🔒并发抓取(模拟)
  12. 教学 08 🔒Task:把协程「派出去」
  13. 教学 09 🔒asyncio.sleep vs time.sleep:阻塞坑
  14. 教学 10 🔒超时控制:wait_for 和 timeout
  15. 教学 11 🔒取消:Task.cancel()
  16. 教学 12 🔒TaskGroup:3.11 之后的 gather 升级版
  17. 教学 13 🔒async with 和 async for
  18. 教学 14 🔒什么时候用 async,什么时候不用
  19. 教学 15 🔒实战:100 个 URL,串行 vs 并发
  20. 教学 16 🔒小结
  21. 教学 17 🔒看清等待和结果,先不比较秒数

学习目录