异步代码已经日益成为Python开发的主流。随着asyncio成为标准库的一部分和许多第三方软件包提供与之兼容的功能,这种模式不会很快消失。
让我们来看看如何使用HTTPX库来利用它来进行异步的HTTP请求,这是非阻塞代码最常见的用例之一。
什么是非阻塞代码?
你可能会听到 "异步"、"非阻塞 "或 "并发 "这样的术语,并对它们的含义感到有点困惑。根据这个更详细的教程,其中两个主要属性是。
- 异步程序能够在等待其最终结果时 "暂停",让其他程序在此期间运行。
- 异步代码,通过上述机制,促进了并发执行。换句话说,异步代码给人以并发的外观和感觉。
因此,异步代码是可以在等待结果时挂起的代码,以便让其他代码在这期间运行。它不会 "阻止 "其他代码的运行,所以我们可以称它为 "非阻塞 "代码。
asyncio库为Python开发者提供了各种工具来实现这一点,而aiohttp为HTTP请求提供了更具体的功能。HTTP请求是非常适合异步性的一个典型例子,因为它们涉及到等待服务器的响应,在这期间,让其他代码运行是很方便和有效的。
设置
在我们开始之前,请确保设置好你的Python环境。如果你需要一些帮助,请按照本指南中的virtualenv部分进行。如果你有多个项目在同一台机器上运行,让一切正常工作,尤其是虚拟环境对于隔离你的依赖关系非常重要。你至少需要Python 3.7或更高的版本,以便运行这篇文章中的代码。
现在你的环境已经设置好了,你将需要安装HTTPX库,用于发出请求,包括异步和同步请求,我们将对此进行比较。在激活你的虚拟环境后,用下面的命令安装它。
pip install httpx==0.18.2
有了这个,你应该准备好继续写一些代码了。
用HTTPX做一个HTTP请求
让我们开始使用HTTPX做一个单一的GET请求,以证明关键字async
和await
工作。我们将使用口袋妖怪API作为一个例子,所以让我们开始尝试获取与传说中的第151个口袋妖怪Mew相关的数据。
运行下面的Python代码,你应该看到名字 "Mew "被打印到终端。
import asyncio
import httpx
async def main():
pokemon_url = 'https://pokeapi.co/api/v2/pokemon/151'
async with httpx.AsyncClient() as client:
resp = await client.get(pokemon_url)
pokemon = resp.json()
print(pokemon['name'])
asyncio.run(main())
在这段代码中,我们正在创建一个叫做main的程序,我们用asyncio事件循环来运行它。在这里,我们正在向Pokemon API发出请求,然后等待响应。
这个async
关键字基本上告诉 Python 解释器,我们定义的 coroutine 应该用一个事件循环异步运行。await
关键字将控制权传回给事件循环,暂停周围的 coroutine 的执行,让事件循环运行其他的东西,直到被 "等待 "的结果被返回。
提出大量的请求
提出一个单一的异步HTTP请求是很好的,因为我们可以让事件循环在其他任务上工作,而不是在等待响应时阻塞整个线程。这个功能在尝试进行大量请求时才真正发挥了作用。让我们通过执行与之前相同的请求来证明这一点,不过是为了所有150个原始小精灵。
让我们把之前的请求代码放在一个循环中,更新哪个小精灵的数据正在被请求,并对每个请求使用await
。
import asyncio
import httpx
import time
start_time = time.time()
async def main():
async with httpx.AsyncClient() as client:
for number in range(1, 151):
pokemon_url = f'https://pokeapi.co/api/v2/pokemon/{number}'
resp = await client.get(pokemon_url)
pokemon = resp.json()
print(pokemon['name'])
asyncio.run(main())
print("--- %s seconds ---" % (time.time() - start_time))
这一次,我们也要测量整个过程需要多少时间。如果你在你的Python shell中运行这段代码,你应该看到类似下面的东西打印到你的终端。
对于150个请求来说,8.6秒似乎很不错,但我们并没有什么可以比较的。让我们试着用同步方式完成同样的事情。
比较同步请求的速度
要像以前一样打印前150个小精灵,但没有async/await,运行以下代码。
import httpx
import time
start_time = time.time()
client = httpx.Client()
for number in range(1, 151):
url = f'https://pokeapi.co/api/v2/pokemon/{number}'
resp = client.get(url)
pokemon = resp.json()
print(pokemon['name'])
print("--- %s seconds ---" % (time.time() - start_time))
你应该看到相同的输出,但运行时间不同。
尽管它看起来并没有比以前慢多少。这很可能是因为HTTPXClient
所做的连接池正在做大部分的重活。然而,我们可以利用更多的asyncio
功能来获得比这更好的性能。
利用asyncio来提高性能
asyncio
提供了更多的工具,可以大大改善我们的整体性能。在原来的例子中,我们在每个单独的HTTP请求后使用await,这不是很理想。我们可以将所有这些请求作为asyncio
任务 "并发 "运行,然后在最后使用asyncio.ensure_future
和asyncio.gather
检查结果。
如果实际发出请求的代码被分解成自己的coroutine函数,我们可以创建一个任务列表,由每个请求的 期货组成。然后,我们可以将这个列表解压到一个 集合调用,将它们全部运行起来。当我们await
这个调用到asyncio.gather
,我们将得到一个所有被传入的期货的可迭代文件,保持它们在列表中的顺序。这样,我们就只用了一次await
。
为了看看我们实现这个方法后会发生什么,请运行以下代码。
import asyncio
import httpx
import time
start_time = time.time()
async def get_pokemon(client, url):
resp = await client.get(url)
pokemon = resp.json()
return pokemon['name']
async def main():
async with httpx.AsyncClient() as client:
tasks = []
for number in range(1, 151):
url = f'https://pokeapi.co/api/v2/pokemon/{number}'
tasks.append(asyncio.ensure_future(get_pokemon(client, url)))
original_pokemon = await asyncio.gather(*tasks)
for pokemon in original_pokemon:
print(pokemon)
asyncio.run(main())
print("--- %s seconds ---" % (time.time() - start_time))
这使我们的时间降到了150个HTTP请求的1.54秒!这比以前有很大的进步。这比之前的例子有了很大的进步。这是完全无阻塞的,所以运行所有150个请求的总时间将大致等于最长请求的运行时间。确切的数字会因你的互联网连接而有所不同。
结论性的想法
正如你所看到的,使用像HTTPX这样的库来重新思考你进行HTTP请求的方式,可以为你的代码增加一个巨大的性能提升,并在进行大量的请求时节省大量的时间。
在这个教程中,我们只是触及了用asyncio可以做什么的表面,但我希望这能使你开始进入异步Python世界的旅程更容易一些。如果你对另一个类似的异步 HTTP 请求库感兴趣,可以看看我写的关于 aiohttp 的另一篇博文。
我期待着看到你所建立的东西。请随时联系我,分享你的经验或提出任何问题。
- 电子邮件:sagnew@twilio.com
- 推特。@Sagnewshreds
- Github。Sagnew
- Twitch(流媒体直播代码)。Sagnewshreds