Выполнение нескольких одновременных HTTP-запросов с Python AsyncIO

Spread the love

Перевод статьи: Steven PateMaking Concurrent HTTP requests with Python AsyncIO

Введение

Начиная с версии 3.4 в Python есть модуль asyncio добавленный в стандартную библиотеку. Asyncio позволяет нам запускать асинхронные задачи, связанные с вводом-выводом. А так же общие задачи, связанные с вводом-выводом, включают вызовы базы данных, чтение и запись файлов на диск, отправку и получение HTTP-запросов. Веб-фреймворк Django – типичный пример приложения с привязкой к вводу-выводу.

В этой статье я продемонстрирую запуск нескольких одновременных HTTP-запросов, для получения информации о ценах на биржевые тикеры. Единственный сторонний пакет, который я буду использовать, – это httpx. Httpx очень похож на популярный пакет requests, но httpx поддерживает asyncio.

Начальные действия

Требуется Python 3.8+

  1. Создайте каталог проекта
  2. Создайте виртуальную среду внутри каталога
    • python3 -m venv async_http_venv
  3. Активировать виртуальную среду
    • source ./async_http_venv/bin/activate
  4. Установите httpx
    • pip install httpx
  5. Скопируйте приведенный ниже пример кода в файл 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
python asyncio http first request

Если вы посмотрите на вывод, запущенные запросы идут не последовательно. В синхронной программе запрос на VTSAX будет первым и завершится первым. После этого начнется следующий запрос VTIAX. В нашей асинхронной программе запросы выполняются один за другим и заканчиваются вне очереди всякий раз, когда API отвечает. Давайте снова запустим сценарий с теми же аргументами и посмотрим, каков порядок результатов.

python asyncio http second request

Как вы можете видеть, в первом запросе мы сначала получили результаты для 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.

Была ли вам полезна эта статья?
[6 / 4]

Spread the love
Подписаться
Уведомление о
guest
1 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Андрей
Андрей
3 лет назад

Так это же не одновременно, это асинхронно в конкурентном режиме.

> awaitable asyncio.gather(*awsloop=Nonereturn_exceptions=False)
Run awaitable objects in the aws sequence concurrently.

“Одновременно” будет, хотя бы, если использовать [Threaded/Process]PoolExecutor.