Выполнение нескольких одновременных HTTP-запросов с Python AsyncIO
Перевод статьи: Steven Pate — Making Concurrent HTTP requests with Python AsyncIO
Введение
Начиная с версии 3.4 в Python есть модуль asyncio добавленный в стандартную библиотеку. Asyncio позволяет нам запускать асинхронные задачи, связанные с вводом-выводом. А так же общие задачи, связанные с вводом-выводом, включают вызовы базы данных, чтение и запись файлов на диск, отправку и получение HTTP-запросов. Веб-фреймворк Django — типичный пример приложения с привязкой к вводу-выводу.
В этой статье я продемонстрирую запуск нескольких одновременных HTTP-запросов, для получения информации о ценах на биржевые тикеры. Единственный сторонний пакет, который я буду использовать, — это httpx. Httpx очень похож на популярный пакет requests, но httpx поддерживает asyncio.
Начальные действия
Требуется Python 3.8+
- Создайте каталог проекта
- Создайте виртуальную среду внутри каталога
python3 -m venv async_http_venv
- Активировать виртуальную среду
source ./async_http_venv/bin/activate
- Установите httpx
pip install httpx
- Скопируйте приведенный ниже пример кода в файл Python с именем async_http.py
Пример кода
import argparse import asyncio import itertools import pprint from decimal import Decimal from typing import List, Tuple import httpx YAHOO_FINANCE_URL = "https://query1.finance.yahoo.com/v8/finance/chart/{}" async def fetch_price( ticker: str, client: httpx.AsyncClient ) -> Tuple[str, Decimal]: print(f"Making request for {ticker} price") response = await client.get(YAHOO_FINANCE_URL.format(ticker)) print(f"Received results for {ticker}") price = response.json()["chart"]["result"][0]["meta"]["regularMarketPrice"] return ticker, Decimal(price).quantize(Decimal("0.01")) async def fetch_all_prices(tickers: List[str]) -> List[Tuple[str, Decimal]]: async with httpx.AsyncClient() as client: return await asyncio.gather( *map(fetch_price, tickers, itertools.repeat(client),) ) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-t", "--tickers", nargs="+", help="List of tickers separated by a space", required=True, ) args = parser.parse_args() loop = asyncio.get_event_loop() result = loop.run_until_complete(fetch_all_prices(args.tickers)) pprint.pprint(result)
Тестируем
Когда только что созданная виртуальная среда активирована и Python файл готов, давайте запустим программу, чтобы протестировать наш код.
python async_http.py -t VTSAX VTIAX IJS VSS AAPL ORCL GOOG MSFT FB
Если вы посмотрите на вывод, запущенные запросы идут не последовательно. В синхронной программе запрос на VTSAX будет первым и завершится первым. После этого начнется следующий запрос VTIAX. В нашей асинхронной программе запросы выполняются один за другим и заканчиваются вне очереди всякий раз, когда API отвечает. Давайте снова запустим сценарий с теми же аргументами и посмотрим, каков порядок результатов.
Как вы можете видеть, в первом запросе мы сначала получили результаты для IJS, но во втором запросе результаты для IJS вернулись четвертыми. Давайте рассмотрим код, чтобы понять, что делает наша программа.
Рассмотрение кода
Начнем с функции fetch_all_prices. Функция начинается с создания AsyncClient, который мы будем передавать каждый раз, когда вызываем fetch_price.
async with httpx.AsyncClient() as client:
Создание клиента позволяет нам использовать пул HTTP-соединений, который повторно использует одно и то же TCP-соединение для каждого запроса. Это увеличивает производительность для каждого HTTP-запроса. Кроме того, мы используем оператор with для автоматического закрытия нашего клиента по завершении функции.
Теперь давайте посмотрим на наш return.
return await asyncio.gather( *map(fetch_price, tickers, itertools.repeat(client),) )
Во-первых, мы запускаем asyncio.gather, который принимает asyncio-функции или корутины. В нашем случае мы расширяем, используя звездочку, map функций fetch_price, которая являются нашей корутиной. Чтобы создать map функцию, мы используем список тикеров tickers и itertools.repeat, которому передается наш клиент каждой функции тикера. Как только наш вызов map завершится, у нас будут функции для каждого тикера, которые мы можем передать в asyncio.gather для одновременного запуска.
Теперь давайте посмотрим на нашу функцию fetch_price.
response = await client.get(YAHOO_FINANCE_URL.format(ticker))
Для выполнения HTTP-запроса GET к Yahoo Finance мы используем AsyncClient. Так же здесь мы используем ключевое слово await, потому что именно здесь происходит ввод-вывод. Как только программа достигает этой строки, она выполняет GET-запрос и передает управление циклу обработки событий.
price = response.json()["chart"]["result"][0]["meta"]["regularMarketPrice"] return ticker, Decimal(price).quantize(Decimal("0.01"))
После завершения запроса мы извлекаем json из ответа и возвращаем цену вместе с тикером, чтобы определить, какая цена связана с тикером. Наконец, прежде чем вернуть цену, мы превращаем ее в десятичную дробь и округляем до ближайших двух десятичных знаков.
Заключение
Экосистема пакетов вокруг модуля asyncio все еще развивается. Httpx выглядит как качественная замена requests. Starlette и FastAPI — два многообещающих веб-сервера на основе ASGI . Начиная с версии 3.1, Django так же поддерживает ASGI. Наконец, с учетом asyncio выпускается больше новых библиотек. На момент написания этой статьи asyncio пока еще не получил широкого распространения, но я предсказываю, что в ближайшие несколько лет asyncio получит гораздо большее распространение в сообществе Python.
Так это же не одновременно, это асинхронно в конкурентном режиме.
> awaitable
asyncio.gather
(*aws, loop=None, return_exceptions=False)Run awaitable objects in the aws sequence concurrently.
«Одновременно» будет, хотя бы, если использовать [Threaded/Process]PoolExecutor.