4 道练习题·预计 35 分钟·做对一题解锁下一段
各位写过爬虫吗?随手一段:
import time
def fake_get(url):
time.sleep(0.01)
return f'GOT {url}'
urls = [f'https://example.com/{i}' for i in range(5)]
results = [fake_get(u) for u in urls]
print(f'拿到 {len(results)} 个')每个请求假装耗时 0.01 秒,5 个串起来就是 0.05 秒。如果是真实网络请求,每个 200 毫秒,100 个串起来就是 20 秒——慢得想骂人。
「那好办,开线程嘛!」有人立刻反应过来,「100 个 URL 就开 100 个线程,一起跑。」
线程当然能解决问题。但开 100 个线程是不是有点奢侈?这 100 个线程 99% 的时间都在干同一件事:等网络包回来。本质上这是个等待问题,不是个计算问题。为了「等」而开 100 个操作系统级的线程,CPU 不开心,内存也不开心。
而且线程一多,「锁、竞态、死锁」这些老朋友就会接连找上门。你只是想抓个网页,怎么忽然要研究操作系统了?
Python 给了我们另一条路:asyncio。它的卖点很直白——
一个线程,同时等一百件事。
各位听到这句话的第一反应大概是:「这不科学吧,单线程怎么同时做一百件事?」
魔法的关键就一句话:
当某个协程在等 IO 的时候,让出 CPU 给其他协程;等回来再继续。
asyncio 没有偷偷开线程,也没有把 CPU 加速。它做的事很朴素——「等」可以重叠。三件事每件等 1 秒,串行要 3 秒;让它们的「等」重叠起来,1 秒就够了。
async def、await、asyncio.run() 这三个最基本的概念asyncio.gather 同时跑多个协程注意:浏览器里的 Python 沙盒不能联网,所以咱们用
asyncio.sleep来模拟「等」的过程。逻辑和真实网络请求是一样的。
普通函数长这样:
def hello():
return '你好'
print(hello()) # 你好加一个 async 关键字,它就成了协程函数(coroutine function):
async def hello():
return '你好'
print(hello())输出大致是这样:
<coroutine object hello at 0x...>
RuntimeWarning: coroutine 'hello' was never awaited
各位看出区别了吗——同样写 print(hello()),普通函数返回的是字符串,协程函数返回的是一个 协程对象,而且还附赠一句警告:「这协程从来没被 await 过」。
async def定义的不是一个「会立刻跑的函数」——它返回一张「待执行的任务单」。
打个比方:
| 函数类型 | f() 的行为 |
|---|---|
| 普通函数 | 立刻派人去执行,立刻拿到结果 |
| 协程函数 | 写一张任务单递给你;什么时候做、谁做,另说 |
这张「任务单」就是协程对象。它必须被 await 或者扔给事件循环,里面的代码才会真正跑起来。
await:让协程跑起来那怎么让任务单真的执行?用 await:
import asyncio
async def hello():
return '你好'
async def main():
result = await hello() # ← 这里让 hello 真正跑
print(result)
asyncio.run(main())输出:
你好
await hello() 的语义是「请帮我把这张任务单完成,然后把结果给我」。
但**await 只能写在 async def 函数体内部**。在普通函数里写 await,Python 会报 SyntaxError: 'await' outside async function。
asyncio.run():进入异步世界的大门「那我想在普通脚本里调一个 async def 怎么办?」答案就是 asyncio.run():
import asyncio
async def main():
print('我在异步世界里')
print('我还在异步世界里')
asyncio.run(main())输出:
我在异步世界里
我还在异步世界里
asyncio.run() 是同步代码和异步代码之间的「门」:
记住一条:整个程序里通常只调一次 asyncio.run()——它就是从同步世界踏进异步世界的入口。
async def + await + asyncio.run到这里,最基础的三件套你已经齐了:
| 工具 | 作用 |
|---|---|
async def f() | 定义协程函数(写出一张任务单的能力) |
await x | 等一张任务单跑完,拿结果 |
asyncio.run(c) | 从同步世界进入异步世界,跑一个协程 |
下一题就用这三个东西写一个最小的 hello world。
请用 async def 定义一个协程函数 hello,函数体打印 'hi'。
然后用 asyncio.run(hello()) 启动它。
输出应该是:
hi