本文转载自微信公众号「区块链研究实验室」,作者链三丰。转载本文请联系区块链研究实验室公众号。
本文中,将向大家介绍如何使用Python异步编程,以便您可以更快地进行更多的API调用。那么让我们开始吧。
请求库
通常,当Python使用者希望进行API调用时,他们会寻找请求库。语法是我最喜欢的语法,因为如果我想进行API调用,则可以运行:
- import requests
- response = requests.get("http://example.com/")
- print(response)
现在,可以做一个for循环:
- import requests
- for i in range(10):
- response = requests.get("http://example.com/")
- print(response)
每次我对进行API调用时example.com,我都必须完成:
- 将请求发送至example.com。
- 等待回应。
- 得到回应。
如果想试图获取大量数据(例如,如果我想从Alpha Vantage API中提取fintech数据),您则需要一个可以设置的免费密钥api_key = your_key_here。
- import requests
- import os
- api_key = os.getenv('ALPHAVANTAGE_API_KEY')
- url = 'https://www.alphavantage.co/query?function=OVERVIEW&symbol={}&apikey={}'
- symbols = ['AAPL', 'GOOG', 'TSLA', 'MSFT', 'PEP']
- results = []
- for symbol in symbols:
- response = requests.get(url.format(symbol, api_key))
- results.append(response.json())
此时必须等待大约1.5秒才能进行5个API调用,然后需要11秒才能进行50个API调用,需要50秒才能进行135个API调用……
如果您想获得2,000家公司或1600万种颜色的数据,我们需要做一些扩展。
异步代码与同步代码
当我们运行Python代码时,我们的过程一行一行地读取代码。在执行一行时,没有其他代码可以运行。这就是所谓的同步代码-依次进行的所有操作。
在异步代码中,我们可以在完成一项任务之前继续执行另一项任务。例如,如果我们考虑同步烹饪汉堡和蔬菜晚餐,我们的“代码”将如下所示:
- cook_burger()
- cook_vegetables()
在这种情况下,因为汉堡是同步的,所以我们要等汉堡完成后才能开始蔬菜。但我们并不总是希望等到汉堡做完之后才能开始烹饪蔬菜。因此我们可以同时煮。一旦完成,我们就可以停止处理成品蔬菜或汉堡的任何工作。在异步代码中,它看起来像这样:
- async def cook_meal():
- await asyncio.gather(cook_burger(), cook_vegetables())
- asyncio.run(cook_meal())
我们“收集”我们将要完成的任务,并await让它们都完成。我们在事件循环中运行它们,以跟踪完成后如何处理它们。您可以不断检查看看其中一个过程是否完成,从而想到事件循环。
现在您可能已经听说过多线程,并且它们是不同的,多线程用于拥有多个工作程序,而异步只有一名工人。
事件循环
回到我们的Alpha Vantage API调用示例。现在,在我们的代码中:
- 发出第一个请求。
- 等待。
- 得到第一反应。
- 发出第二个请求。
- 等待。
- 得到第二个答复。
如果我们有五个符号,我们将“等待”五次。那么我们需要代替执行此操作,启动一个API调用,然后启动其他API调用,最后再处理响应。
另外,除了执行上述操作之外,我们还可以:
- 发出第一个请求。
- 发出第二个请求。
- 等待。
- 得到第一反应。
- 得到第二个答复。
在第二个示例中,我们只有一个等待时间!当返回响应时(可能在我们发出请求时发生),因此我们需要一些处理返回的响应的方法,这被称为事件循环。
事件循环会定期检查以查看我们的异步操作是否已返回,并安排它们进行相应的处理。当我们正常运行Python时,没有运行任何事件循环来处理该事件,因此我们需要设置事件循环,以便可以按顺序处理响应。
然后,我们可以异步运行我们的代码。
输入asyncio和aiohttp
我们现在知道,当我们异步运行代码时,我们无须等待代码操作完成,我们可以使用asyncio和aiohttp来进行操作。
- import asyncio
- import aiohttp
- import os
- import time
- api_key = os.getenv('ALPHAVANTAGE_API_KEY')
- url = 'https://www.alphavantage.co/query?function=OVERVIEW&symbol={}&apikey={}'
- symbols = ['AAPL', 'GOOG', 'TSLA', 'MSFT', 'PEP']
- results = []
- async def get_symbols():
- async with aiohttp.ClientSession() as session:
- for symbol in symbols:
- response = await session.get(url.format(symbol, api_key), ssl=False)
- asyncio.run(get_symbols())
分解
我们将使用asyncio.run(get_symbols()),这会促使事件循环的启动,并且会允许我们使用异步代码。
此时您会注意到,在以往许多的示例中,它们如何启动事件循环会更加明确:
- loop = asyncio.get_event_loop()
- results = loop.run_until_complete(get_symbols())
- loop.close()
此代码块的作用与asyncio.run(get_symbols())完全相同,那是我们的切入点。然后我们转到函数:
- async def get_symbols():
- async with aiohttp.ClientSession() as session:
- for symbol in symbols:
- response = await session.get(url.format(symbol, api_key), ssl=False)
我们必须从async关键字开始,这使Python知道此函数将是异步的,并且我们可以使用事件循环。
我们将展开一个会话aiohttp,aiohttp是异步版本requests。
我们按照相同的方式进行操作,并调用aiohttp版本的request.get(即session.get),此处需要添加内容ssl=False。
由于session.get是异步函数(也称为协程),因此我们必须await做出响应,否则它们会返回协程本身。
现在我们已经请求代码复制为异步语法,此时我们依然需要等待。
收集任务
我们即将要启动所有API调用。
- import asyncio
- import aiohttp
- import os
- import time
- api_key = os.getenv('ALPHAVANTAGE_API_KEY')
- url = 'https://www.alphavantage.co/query?function=OVERVIEW&symbol={}&apikey={}'
- symbols = ['AAPL', 'GOOG', 'TSLA', 'MSFT', 'PEP']
- results = []
- def get_tasks(session):
- tasks = []
- for symbol in symbols:
- tasks.append(session.get(url.format(symbol, api_key), ssl=False))
- return tasks
- async def get_symbols():
- async with aiohttp.ClientSession() as session:
- tasks = get_tasks(session)
- responses = await asyncio.gather(*tasks)
- asyncio.run(get_symbols())
我们有一个名为的全新功能get_tasks。此功能将所有协同程序合并到一个列表中,以便我们立即启动。请记住,此列表中的所有函数都必须是异步函数或已放置在事件队列中的任务。
我们还可以通过以下方式获得所有任务:
- tasks = [session.get(URL.format(symbol, API_KEY), ssl=False) for symbol in symbols]
在得到要启动的功能/任务的列表后,我们可以get_symbols使用以下命令在功能中将它们全部启动:
- responses = await asyncio.gather(*tasks)
我们将等待所有任务完成并将它们放入responses对象中。
- responses = await asyncio.gather(session.get(URL.format('IBM', API_KEY), ssl=False), session.get(URL.format('AAPL', API_KEY), ssl=False), session.get(URL.format('MSFT', API_KEY), ssl=False))
因为*tasks只是将列表解引用为变量的一种方法。
我们“收集”所有任务并将其运送出去,当它们响应时,事件循环将它们拾取,并在我们交付所有任务后将它们放入要处理的队列中。
协程与任务
在上面的示例中,我们向asyncio.gather函数传递了异步协程列表,以便可以将它们调度到事件循环中,实际上可以更快地将它们调度到事件循环中!
在我们的get_tasks函数中,我们调用了:
- tasks.append(session.get(url.format(symbol, api_key), ssl=False))
我们将该session.get函数添加到了任务列表中,并且仅在调用时将它们添加到了事件循环中gather。实际上,您可以使用asyncio.create_task以下命令更快地将其添加到事件循环中:
- tasks.append(asyncio.create_task(session.get(url.format(symbol, api_key), ssl=False)))
这会将session.get函数添加到事件循环中,并且asyncio.gather函数将等待该任务完成。
请记住它们的不同之处。协程是函数,而任务则是在事件循环中安排的任务。asyncio.gather将等待任务返回和/或将协程安排到事件循环中,并等待它们返回。