Асинхронный Python в веб-разработке | Asyncio и Aiohttp

Асинхронное программирование хорошо подходит для задач, которые включают частое чтение и запись файлов или отправку данных с сервера туда и обратно. Асинхронные программы выполняют операции ввода-вывода неблокирующим образом, а это означает, что они могут выполнять другие задачи, ожидая возврата данных от клиента, а не просто ждать, тратя впустую ресурсы и время.

Python, как и многие другие языки, по умолчанию не является асинхронным. К счастью, быстрые изменения в мире ИТ позволяют нам писать асинхронный код даже на языках, которые изначально для этого не предназначались.

Неблокирующее поведение асинхронных программ может привести к значительному повышению производительности в контексте веб-приложения, помогая решить проблему разработки реактивных приложений.

В Python 3 встроено несколько мощных инструментов для написания асинхронных приложений. В этой статье мы рассмотрим некоторые из этих инструментов.

Введение в асинхронный Python

Для тех, кто знаком с написанием традиционного кода Python, переход к асинхронному коду может быть концептуально немного сложным. Асинхронный код в Python опирается на сопрограммы(coroutines), которые в сочетании с циклом обработки событий позволяют писать код, который может выполнять несколько действий одновременно.

Сопрограммы можно рассматривать как функции, в коде которых есть точки, в которых они возвращают управление программой вызывающему контексту. Эти точки позволяют приостанавливать и возобновлять выполнение сопрограммы в дополнение к обмену данными между контекстами.

Цикл событий решает, какой фрагмент кода выполняется в любой момент — он отвечает за приостановку, возобновление и взаимодействие между сопрограммами. Это означает, что части разных сопрограмм могут выполняться не в том порядке, в котором они были запланированы. Эта идея выполнения разных фрагментов кода не по порядку называется параллелизмом.

Размышление о параллелизме в контексте выполнения HTTP запросов может многое прояснить. Представьте, что вы хотите сделать много независимых запросов к серверу. Например, мы могли бы запросить веб-сайт, чтобы получить статистику обо всех спортивных игроках в данном сезоне.

Мы могли бы сделать каждый запрос последовательно. Однако мы можем представить, что с каждым запросом наш код может потратить некоторое время на ожидание доставки запроса на сервер и отправки ответа обратно.

Иногда эти операции могут занимать даже несколько секунд. Приложение может испытывать задержки в сети из-за большого количества пользователей или просто из-за ограничений скорости данного сервера.

Что, если бы наш код мог делать другие вещи, ожидая ответа от сервера? Более того, что, если он вернется к обработке данного запроса только после получения данных ответа? Мы могли бы сделать много запросов в быстрой последовательности, если бы нам не приходилось ждать завершения каждого отдельного запроса, прежде чем переходить к следующему в списке.

Сопрограммы с циклом событий позволяют нам писать код, который ведет себя именно таким образом.

Asyncio

asyncio , часть стандартной библиотеки Python, предоставляет цикл обработки событий и набор инструментов для управления им. С помощью asyncio мы можем планировать выполнение сопрограмм и создавать новые сопрограммы (на самом деле asyncio.Task объекты, используя терминологию asyncio ), которые завершат выполнение только после завершения выполнения составных сопрограмм.

В отличие от других языков асинхронного программирования, Python не заставляет нас использовать цикл обработки событий, который поставляется вместе с языком. Сопрограммы Python представляют собой асинхронный API, с помощью которого мы можем использовать любой цикл обработки событий.

Существуют проекты, которые реализуют совершенно другой цикл событий, например curio , или позволяют использовать другую политику цикла событий для asyncio (политика цикла событий — это то, что управляет циклом событий «за кулисами»), например uvloop .

Давайте взглянем на фрагмент кода, который одновременно запускает две сопрограммы, каждая из которых выводит сообщение через одну секунду:

# example1.py
import asyncio

async def wait_around(n, name):
    for i in range(n):
        print(f"{name}: iteration {i}")
        await asyncio.sleep(1.0)

async def main():
    await asyncio.gather(*[
        wait_around(2, "coroutine 0"), wait_around(5, "coroutine 1")
    ])

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
me@local:~$ time python example1.py
coroutine 1: iteration 0
coroutine 0: iteration 0
coroutine 1: iteration 1
coroutine 0: iteration 1
coroutine 1: iteration 2
coroutine 1: iteration 3
coroutine 1: iteration 4

real    0m5.138s
user    0m0.111s
sys     0m0.019s

Этот код выполняется примерно за 5 секунд, поскольку asyncio.sleep сопрограмма устанавливает точки, в которых цикл обработки событий может перейти к выполнению другого кода. Более того, мы указали циклу событий запланировать оба wait_around экземпляра для одновременного выполнения с asyncio.gather функцией.

Asyncio.gather берет список «ожидаемых» (т. е. сопрограмм или asyncio.Task объектов) и возвращает один asyncio.Task объект, который завершается только после завершения всех составляющих его задач/сопрограмм. Последние две строки являются asyncioшаблоном для запуска данной сопрограммы до ее завершения.

Сопрограммы, в отличие от функций, не начинают выполняться сразу после вызова. Ключевое await слово — это то, что говорит циклу обработки событий запланировать выполнение сопрограммы.

Если мы уберем await перед asyncio.sleep, программа завершится (почти) мгновенно, так как мы не сказали циклу обработки событий фактически выполнить сопрограмму, что в данном случае говорит сопрограмме приостановить работу на заданное время.

Поняв, как выглядит асинхронный код Python, давайте перейдем к асинхронной веб-разработке.

Установка aiohttp

aiohttp — это библиотека Python для выполнения асинхронных HTTPзапросов. Кроме того, он предоставляет основу для сборки серверной части веб-приложения. Используя Python 3.5+ и pip, мы можем установить aiohttp :

pip install --user aiohttp

Клиентская сторона: создание запросов

В следующих примерах показано, как мы можем загрузить HTML-контент веб-сайта «example.com» с помощью aiohttp :

# example2_basic_aiohttp_request.py
import asyncio
import aiohttp

async def make_request():
    url = "https://example.com"
    print(f"making request to {url}")
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            if resp.status == 200:
                print(await resp.text())

loop = asyncio.get_event_loop()
loop.run_until_complete(make_request())

Несколько вещей, которые следует подчеркнуть:

  • Как и with, await asyncio.sleep мы должны использовать await with resp.text(), чтобы получить HTML-контент страницы. Если бы мы его опустили, вывод нашей программы был бы примерно таким:
me@local:~$ python example2_basic_aiohttp_request.py
<coroutine object ClientResponse.text at 0x7fe64e574ba0>
  • async with— менеджер контекста, который работает с сопрограммами вместо функций. В обоих случаях, когда он используется, мы можем представить себе, что внутри aiohttp закрывает соединения с серверами или иным образом освобождает ресурсы.
  • aiohttp.ClientSessionимеет методы, соответствующие HTTP- глаголам. Таким же
  • образом, как и session.getзапрос GET , session.postбудет выполнен запрос POST .

Этот пример сам по себе не дает преимущества в производительности по сравнению с синхронными HTTP-запросами. Настоящая красота aiohttp на стороне клиента заключается в выполнении нескольких одновременных запросов:

# example3_multiple_aiohttp_request.py
import asyncio
import aiohttp

async def make_request(session, req_n):
    url = "https://example.com"
    print(f"making request {req_n} to {url}")
    async with session.get(url) as resp:
        if resp.status == 200:
            await resp.text()

async def main():
    n_requests = 100
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(
            *[make_request(session, i) for i in range(n_requests)]
        )

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

Вместо того, чтобы делать каждый запрос последовательно, мы просим asyncio делать их одновременно с asycio.gather.

Заключение

В этой статье мы рассмотрели, как выглядит асинхронная веб-разработка на Python, ее преимущества и способы использования.