import asyncio
import aiohttp
import time
start = time.time()
async def get(url):
session = aiohttp.ClientSession()
response = await session.get(url)
await response.text()
await session.close()
return response
async def request():
url = 'https://www.httpbin.org/delay/5'
print('Waiting for',url)
response = await get(url)
print('Get response from',url,'response',response)
tasks = [asyncio.ensure_future(request()) for _ in range(10)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print('Cost time:',end - start)
aiohttp流程分析:
1. session = aiohttp.ClientSession()
这行代码的作用是创建一个 aiohttp 库的客户端会话实例 session ,该实例是发起异步 HTTP 请求的核心载体,后续的 GET 请求都通过这个会话对象执行。
- 该会话会管理底层的连接池、TCP 连接复用等资源,是
aiohttp异步 HTTP 请求的基础入口。 - 此处仅完成实例化,尚未建立实际的网络连接,网络操作会在后续的
get方法中触发。
2. response = await session.get(url)
这行代码是发起异步的 HTTP GET 请求,并等待请求完成以获取响应对象 response。
- 关键字
await表明这是一个可等待(awaitable)的异步操作,会暂停当前协程的执行,直到 GET 请求完成(获取到服务端响应或请求失败),再恢复协程执行并返回响应对象。 - 执行该方法时,会利用第一步创建的会话
session管理的资源,建立网络连接、发送 GET 请求、接收服务端响应头,最终返回封装了响应状态、响应头、响应体等信息的ClientResponse实例。 - 此时仅获取了响应的元数据(状态码、响应头等),响应体(正文内容)并未被完整读取到内存中,只是建立了读取响应体的通道。
3. await response.text()
这行代码是异步读取并解析 HTTP 响应的正文内容为字符串格式 ,但存在一个关键特点:仅执行了读取操作,却没有将读取到的字符串结果进行保存或使用。
response.text()是异步方法,需要通过await等待读取完成,它会自动根据响应头中的Content-Type推断编码格式(默认 utf-8),将二进制的响应体数据解码为字符串。- 该方法执行时,才会完整读取服务端返回的响应体数据到内存中,但由于代码中没有赋值给任何变量(如
content = await response.text()),读取完成后的字符串结果会被立即丢弃,后续无法访问该响应内容。 - 补充:若响应体较大,该方法会将全部内容加载到内存,此处仅体现 "读取但不保存" 的行为特征。
4. await session.close()
这行代码是异步关闭之前创建的 ClientSession 会话实例,释放该会话占用的所有资源。
- 会话关闭操作是异步的,需要
await等待关闭完成,确保资源(连接池、空闲 TCP 连接、文件句柄等)被正确释放,避免资源泄露。 - 关闭后的
session实例无法再用于发起新的 HTTP 请求,若后续尝试调用session.get()等方法,会抛出异常。
代码优化分析:
首先,这几行代码是 Python 3.7 及更早版本的异步任务执行写法,在 Python 3.7+ 中提供了更简洁、更推荐的现代写法,同时需要先明确原代码的核心功能:创建 10 个异步任务,提交到事件循环并等待所有任务执行完成。
优化方案(分版本,推荐现代写法)
方案 1:Python 3.7+ 推荐(简洁、优雅,无需手动操作事件循环)
使用 asyncio.run() 替代手动获取 / 管理事件循环,这是 Python 3.7 引入的高层级 API,会自动完成「创建事件循环 → 运行任务 → 关闭事件循环」的全流程,避免手动操作的遗漏。
同时,原代码中的 asyncio.ensure_future() 也可替换为 asyncio.create_task()(Python 3.7+),语义更清晰,专门用于创建异步任务。
优化后的对应代码:
# 替代原有的 3 行异步任务执行代码
async def main():
# 创建10个异步任务(替代 asyncio.ensure_future())
tasks = [asyncio.create_task(request()) for _ in range(10)]
# 等待所有任务执行完成(等价于原 asyncio.wait(tasks))
await asyncio.wait(tasks)
# 自动管理事件循环,运行主协程
asyncio.run(main())
方案 2:兼容 Python 3.7 以下(优化原写法,更严谨)
若需兼容低版本,原写法可优化两点:
- 手动获取事件循环后,最终关闭事件循环(原代码缺失
loop.close(),可能导致资源泄露) - 保持
asyncio.ensure_future(),完善资源回收
优化后的对应代码:
tasks = [asyncio.ensure_future(request()) for _ in range(10)]
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(asyncio.wait(tasks))
finally:
# 新增:关闭事件循环,释放资源
loop.close()
完整优化后代码(Python 3.7+ 完整版)
结合上述优化,同时保留原代码的核心业务逻辑,完整代码如下:
import asyncio
import aiohttp
import time
start = time.time()
async def get(url):
session = aiohttp.ClientSession()
response = await session.get(url)
await response.text()
await session.close()
return response
async def request():
url = 'https://www.httpbin.org/delay/5'
print('Waiting for', url)
response = await get(url)
print('Get response from', url, 'response', response)
# 新增主协程,统一管理任务
async def main():
tasks = [asyncio.create_task(request()) for _ in range(10)]
await asyncio.wait(tasks)
if __name__ == "__main__":
# 现代异步任务执行入口
asyncio.run(main())
end = time.time()
print('Cost time:', end - start)
关键优化点解析
-
asyncio.run()的核心优势- 高层级 API,无需手动调用
asyncio.get_event_loop()、loop.run_until_complete()、loop.close() - 自动处理事件循环的创建与销毁,避免资源泄露
- 语义清晰,一行代码完成异步任务的入口执行,是 Python 3.7+ 异步编程的标准入口
- 高层级 API,无需手动调用
-
asyncio.create_task()与asyncio.ensure_future()的区别- 两者均可创建异步任务,返回
Task对象 asyncio.create_task()(Python 3.7+):语义更明确,专门用于「创建新任务」,推荐优先使用asyncio.ensure_future():兼容更早版本,不仅能创建任务,还能将任意可等待对象(awaitable)包装为Future对象,适用场景更广泛
- 两者均可创建异步任务,返回
总结
- Python 3.7+ 优先使用
asyncio.run()+asyncio.create_task(),代码更简洁、更安全 asyncio.run()自动管理事件循环,避免手动操作的遗漏- 低版本兼容可保留原写法,补充
loop.close()完善资源回收 - 优化后的异步任务执行代码,功能与原代码一致,但可读性和健壮性更强
代码升级:
import aiohttp
import asyncio
async def fetch(session,url):
async with session.get(url) as response:
return await response.text(),response.status
async def main():
async with aiohttp.ClientSession() as session:
html,status = await fetch(session,'https://cuiqingcai.com')
print(f'html:{html[:100]}...')
print(f'status:{status}')
if __name__=='__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
关键知识点:
1、异步场景下的 async with...as
async with,这是 Python 3.5+ 引入的异步上下文管理器 ,专门用于异步编程场景,与普通 with...as 功能一致(自动管理异步资源),但有明显区别:
核心区别
| 特性 | 普通 with...as |
异步 async with...as |
|---|---|---|
| 适用场景 | 同步编程(普通函数) | 异步编程(async def 异步函数) |
| 实现协议 | 实现 __enter__() 和 __exit__() |
实现 __aenter__() 和 __aexit__()(异步魔法方法) |
| 关键字 | with、as |
async with、as(多了 async 关键字) |
| 执行方式 | 同步执行(阻塞) | 异步执行(不阻塞事件循环) |
2、理解 html,status = await fetch(session,'https://cuiqingcai.com') 这行代码的含义、语法
一、整体语法拆解:多重核心语法的组合
这行代码同时包含了 3 个 Python 核心语法,缺一不可,整体作用是获取异步函数 fetch 的返回结果,并分别赋值给 html 和 status 两个变量:
- 解包赋值(元组解包):
html, status = ... - 异步等待关键字:
await - 函数调用:
fetch(session,'https://cuiqingcai.com')
二、逐部分详解
1. 右侧:await fetch(session,'https://cuiqingcai.com')
(1)fetch(session,'https://cuiqingcai.com'):调用异步函数
fetch 是 异步函数(用 async def 声明),这里传入了两个参数:
- 第一个参数
session:aiohttp.ClientSession()实例(异步 HTTP 会话对象),用于发起异步 HTTP 请求,保证请求的高效性和会话复用。 - 第二个参数
'https://cuiqingcai.com':目标请求 URL,即要获取内容的网页地址。
⚠️ 注意:直接调用异步函数(不加 await),不会执行函数内部的逻辑,只会返回一个 coroutine(协程对象),无法得到函数的实际返回结果。
(2)await:等待异步函数执行完成并获取返回值
await 是 Python 异步编程中的核心关键字,它的使用有严格限制(只能在 async def 声明的异步函数内部使用 ,这里 main 函数正是 async def 声明的,符合要求)。
它的核心作用是:
- 暂停当前异步函数(
main)的执行流程,不会阻塞整个事件循环(这是异步编程高效的关键)。 - 等待被调用的异步函数(
fetch)执行完毕,直到获取到fetch的实际返回结果。 - 当
fetch执行完成后,恢复main函数的执行流程,继续向下执行后续代码(打印html和status)。
简单说:await 就是 "等待异步函数执行完成,拿到它的返回值",且不会阻塞其他异步任务的执行。
2. 左侧:html, status = ...:元组解包赋值(序列解包)
这是 Python 的 ** 元组解包(也叫序列解包)** 语法,专门用于将一个可迭代对象(这里是 fetch 函数返回的元组)的元素,分别赋值给多个变量,要求「变量个数」与「可迭代对象的元素个数」完全一致。
对应到 fetch 函数的返回值:
# fetch 函数的返回语句,返回了两个值,本质上是一个隐式元组 (response.text(), response.status)
return await response.text(), response.status
- Python 中,多个值用逗号分隔返回时,会自动打包成一个 元组(tuple) (即使没有加小括号
())。 - 左侧的
html和status两个变量,会按照顺序依次接收元组中的两个元素:- 第一个变量
html:接收await response.text()的结果(目标网页的完整 HTML 源代码,字符串类型)。 - 第二个变量
status:接收response.status的结果(HTTP 响应状态码,整数类型,例如 200 表示请求成功,404 表示页面不存在)。
- 第一个变量
等价于显式元组解包(更易理解):
# 先获取 fetch 返回的元组
response_result = await fetch(session,'https://cuiqingcai.com')
# 再解包赋值
html = response_result[0]
status = response_result[1]