Python 从入门到精通
第 32 / 36 篇
从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。
-
01
Python 怎么装、怎么用 -
02
变量、数字和字符串 -
03
列表:把一组数据放在一起 -
04
元组、集合和字典 -
05
条件判断:让程序做选择 -
06
循环:重复的事交给程序 -
07
函数:把代码整理成可复用的块 -
08
模块与包:拆分你的程序 -
09
输入、输出与字符串格式化 -
10
文件读写:保存程序的数据 -
11
异常处理:程序出错时怎么办 -
12
基础阶段练习:命令行记账本 -
13
类和对象:面向对象入门 -
14
继承、组合与特殊方法 -
15
迭代器与生成器 -
16
列表推导式与生成器表达式 -
17
装饰器:给函数增加能力 -
18
上下文管理器与 with -
19
类型标注与 dataclass -
20
正则表达式:从文本中找规律 -
21
日期、时间与时区 -
22
日志与调试 -
23
虚拟环境与依赖管理 -
24
测试:让修改不再提心吊胆 -
25
网络请求:用 Python 调用 API -
26
网页解析与合规采集 -
27
操作 Excel、CSV 与批量文件 -
28
SQLite:给程序加一个数据库 -
29
数据分析入门:NumPy 与 Pandas -
30
画图:把数据变得直观 -
31
Flask 入门:做一个小网站 -
32
异步编程:同时处理多项任务正在阅读 -
33
线程、进程与并发选择 -
34
性能分析与优化 -
35
项目结构、配置与发布 -
36
综合项目:从需求到上线
你的程序要同时做很多事:一边下载文件、一边处理数据、一边等用户输入。如果一件件排队做,光等网络就能等到天荒地老。异步编程(asyncio)就是解决”等待型任务”的利器——等待的时候去做别的事。
为什么需要异步
先看一个痛点:抓 5 个网页,同步写法要等完一个再等下一个:
import time
import requests
urls = ["https://www.nsn0704y.cn"] * 5
start = time.time()
for url in urls:
requests.get(url, timeout=10) # 每个等 0.5 秒,5 个就是 2.5 秒
print(f"同步耗时:{time.time() - start:.2f} 秒")
网络请求大部分时间都在等(对方响应)。等的时候 CPU 闲着,纯浪费。异步的思路:发出去请求,不等它回来,先去发下一个,都发完了再统一收结果。
async / await:两个关键字
import asyncio
async def say_hello():
print("你好")
await asyncio.sleep(1) # 模拟等待(比如等网络)
print("一秒后")
# 直接调用不会执行!async 函数返回协程对象
# say_hello() ← 啥也不会发生
# 要用事件循环跑起来
asyncio.run(say_hello())
规则:
async def定义协程(可暂停的函数)await后面跟”可能等待”的操作,遇到等待就让出控制权去干别的- 协程必须用
asyncio.run()或事件循环启动,不能直接调用
并发执行:gather
asyncio.gather 让多个协程同时跑,一起等结果:
import asyncio
async def fetch_one(name, seconds):
print(f"{name} 开始")
await asyncio.sleep(seconds) # 模拟网络等待
print(f"{name} 完成")
return f"{name} 的数据"
async def main():
# 三个协程并发执行,总耗时 ≈ 最长的那个(3 秒),不是 1+2+3=6 秒
results = await asyncio.gather(
fetch_one("任务A", 1),
fetch_one("任务B", 2),
fetch_one("任务C", 3),
)
print(results) # ['任务A 的数据', '任务B 的数据', '任务C 的数据']
asyncio.run(main())
任务A 开始
任务B 开始
任务C 开始
任务A 完成
任务B 完成
任务C 完成
看到没:三个任务”同时”开始(几乎同时打印”开始”),总时间从 6 秒变 3 秒。这就是异步的威力。
aiohttp:异步版 requests
requests 是同步库,不能配合 await。异步网络请求用 aiohttp:
pip install aiohttp
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as resp:
return resp.status, url
async def main():
urls = [
"https://www.nsn0704y.cn",
"https://www.baidu.com",
"https://www.bilibili.com",
]
async with aiohttp.ClientSession() as session:
# 并发抓取所有 URL
results = await asyncio.gather(
*(fetch(session, url) for url in urls)
)
for status, url in results:
print(f"{status} {url}")
asyncio.run(main())
和同步代码长得几乎一样,只是:session.get 前加 await、用 async with、外面套 async def。
实战:并发抓取提速对比
import asyncio
import time
import aiohttp
async def fetch_one(session, url):
async with session.get(url, timeout=10) as resp:
return len(await resp.text())
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
return await asyncio.gather(*(fetch_one(session, u) for u in urls))
urls = ["https://www.nsn0704y.cn"] * 10
start = time.time()
sizes = asyncio.run(fetch_all(urls))
print(f"异步 10 个请求:{time.time() - start:.2f} 秒")
# 对比:同步 requests 逐个来
import requests
start = time.time()
for u in urls:
requests.get(u, timeout=10)
print(f"同步 10 个请求:{time.time() - start:.2f} 秒")
网络环境好的话,异步能快 5-10 倍。注意频率控制:抓别人网站时别并发太高,加个信号量限流(asyncio.Semaphore)。
超时和异常
import asyncio
async def slow_task():
await asyncio.sleep(10)
return "太慢了"
async def main():
try:
# 超时控制:等 2 秒,等不到就取消
result = await asyncio.wait_for(slow_task(), timeout=2)
print(result)
except asyncio.TimeoutError:
print("任务超时了")
# 单个协程出错,gather 默认会抛异常
try:
await asyncio.gather(
maybe_fail(1),
maybe_fail(0), # 这个会出错
)
except ValueError as e:
print(f"捕获错误:{e}")
# 想忽略单个错误用 return_exceptions=True:
# results = await asyncio.gather(..., return_exceptions=True)
async def maybe_fail(x):
if x == 0:
raise ValueError("x 不能是 0")
return x
asyncio.run(main())
网络代码的三大件在异步里一样不能少:超时、异常处理、频率控制。
异步 vs 多线程:怎么选
很多人在这里纠结,一句话分清:
- IO 密集(等网络、等磁盘、等数据库):用异步——等待时让出 CPU,单线程就能扛大量并发
- CPU 密集(大量计算、数据处理):用多进程/多线程(下篇讲)——需要多核并行
- 混合场景:异步为主,计算部分用线程池
asyncio.to_thread()丢出去
记不住就记:等得多的用异步,算得多的用多进程。
新手坑
坑 1:忘了 await。写 session.get(url) 没加 await,返回的是协程对象不是响应,运行时报”coroutine was never awaited”警告。检查每个异步调用。
坑 2:在异步里用同步库。requests 不能配合 await,会阻塞整个事件循环(等于所有任务排队等它)。用 aiohttp。
坑 3:并发太高被 ban。抓网站用 asyncio.Semaphore(5) 限流,别一股脑全发。
小结
async def定义协程,await暂停等待,asyncio.run()启动asyncio.gather并发跑多个协程,总耗时≈最慢那个- 异步网络用 aiohttp(requests 会阻塞事件循环)
- 超时 wait_for、异常 return_exceptions、限流 Semaphore
- 选型:IO 密集用异步,CPU 密集用多进程
练习
- 用 aiohttp 并发请求 5 个 URL,统计总耗时,和同步版对比
- 用 asyncio.Semaphore(3) 限流,写一个”每次最多 3 个并发”的抓取任务
- 思考:为什么在 async 函数里调用同步的 requests.get 会拖慢所有任务?”事件循环”在这中间扮演什么角色?