Solicitações HTTP assíncronas em Python com aiohttp e asyncio

March 25, 2021
Escrito por
Sam Agnew
Twilion

Solicitações HTTP assíncronas em Python com aiohttp e asyncio

O código assíncrono tornou-se cada vez mais um pilar do desenvolvimento com Python. Com asyncio se tornando parte da biblioteca padrão e os muitos pacotes de terceiros que fornecem recursos compatíveis, esse paradigma não deve desaparecer tão cedo.

Vamos aprender a usar a biblioteca aiohttp para fazer solicitações HTTP assíncronas, que é um dos casos de uso mais comuns do código sem bloqueio.

O que é código sem bloqueio?

Talvez você ouça termos como "assíncrono", "sem bloqueio" ou "simultâneo" e fique um pouco confuso com o significado deles. De acordo com este tutorial mais detalhado, duas das principais propriedades são:

  • É possível "pausar" rotinas assíncronas enquanto aguarda o resultado para que outras rotinas sejam executadas nesse intervalo.
  • código assíncrono, por meio do mecanismo acima, facilita a execução simultânea. Em outras palavras, o código assíncrono confere um aspecto de simultaneidade.

Por isso, o código assíncrono é aquele que pode ser suspenso enquanto se espera um resultado para que outro código possa ser executado nesse meio tempo. Ele não "bloqueia" a execução de outro código, por isso é possível chamar de código "sem bloqueio".

biblioteca asyncio oferece diversas ferramentas para os desenvolvedores Python criarem esse código, e a aiohttp tem uma funcionalidade ainda mais específica para solicitações HTTP. As solicitações HTTP são um exemplo clássico de algo que é bem adequado à assincronicidade porque envolvem esperar uma resposta de um servidor, período durante o qual seria conveniente e eficiente ter outro código em execução.

Configuração

Antes de começar, verifique se o ambiente Python está configurado. Se precisar de ajuda, siga este guia até a seção virtualenv. É importante deixar tudo funcionando de forma correta, principalmente no que diz respeito a ambientes virtuais para isolar as dependências caso tenha vários projetos em execução no mesmo computador. É necessário, pelo menos, o Python 3.7 ou superior para executar o código desta publicação.

Agora que o ambiente está configurado, é preciso instalar algumas bibliotecas de terceiros. Vamos usar a aiohttp para fazer solicitações assíncronas e a biblioteca requests para fazer solicitações HTTP síncronas comuns e comparar depois as duas. Instale as duas bibliotecas usando o seguinte comando depois de ativar o ambiente virtual:

pip install aiohttp-3.7.4.post0 requests==2.25.1

Após essas ações, você está pronto para prosseguir e gravar alguns códigos.

Como fazer uma solicitação HTTP com aiohttp

Comece fazendo uma única solicitação GET usando aiohttp para demonstrar como funcionam as palavras-chave async e await. Usemos a API Pokémoncomo exemplo e, para começar, tente obter os dados associados ao lendário 151º Pokémon, Mew.

Execute o seguinte código Python para visualizar o nome "mew" exibido no terminal:

import aiohttp
import asyncio


async def main():

    async with aiohttp.ClientSession() as session:

        pokemon_url = 'https://pokeapi.co/api/v2/pokemon/151'
        async with session.get(pokemon_url) as resp:
            pokemon = await resp.json()
            print(pokemon['name'])

asyncio.run(main())

Neste código, é criada uma corrotina chamada main, que está sendo executada com o loop de eventos asyncio. Aqui, estamos iniciando uma sessão do cliente aiohttp, um objeto único que pode ser usado para várias solicitações individuais e que, por padrão, pode se conectar com até 100 servidores diferentes ao mesmo tempo. Com esta sessão, estamos fazendo uma solicitação à API Pokémon e aguardando uma resposta.

Esta palavra-chave async basicamente informa ao interpretador Python que a corrotina definida deve ser executada de maneira assíncrona com um loop de eventos. A palavra-chave await devolve o controle ao loop de eventos, o que suspende a execução da corrotina adjacente e permite que o loop de eventos execute outras funções até receber o resultado que está sendo "aguardado".

Como fazer muitas solicitações

É ótimo fazer uma única solicitação HTTP assíncrona porque o loop de eventos pode trabalhar em outras tarefas, em vez de bloquear todo o thread enquanto aguarda uma resposta. Mas o grande destaque dessa funcionalidade é quando tenta fazer um número maior de solicitações. Vamos fazer uma demonstração executando a mesma solicitação anterior, mas para todos os 150 do Pokémon original.

Vejamos o código da solicitação anterior e colocamos em um loop, o que atualiza os dados do Pokémon que estão sendo solicitados e usando await para cada solicitação:

import aiohttp
import asyncio
import time

start_time = time.time()


async def main():

    async with aiohttp.ClientSession() as session:

        for number in range(1, 151):
            pokemon_url = f'https://pokeapi.co/api/v2/pokemon/{number}'
            async with session.get(pokemon_url) as resp:
                pokemon = await resp.json()
                print(pokemon['name'])

asyncio.run(main())
print("--- %s seconds ---" % (time.time() - start_time))

Desta vez, também vamos calcular a duração de todo processo. Se você executar este código no shell do Python, aparecerá algo com o seguinte no terminal:

Resultado de 150 chamadas de API usando aiohttp

Oito segundos parece muito bom para 150 solicitações, mas não temos nada para comparar. Vamos tentar fazer a mesma coisa de maneira síncrona com a biblioteca requests.

Comparação da velocidade com solicitações síncronas

A Requests foi criada para ser uma biblioteca HTTP "para humanos", por isso tem uma API muito bonita e simplista. Recomendo muito a qualquer projeto em que a velocidade possa não ser de fundamental importância, se comparada a um código fácil de usar e seguir.

Para ter os 150 primeiros Pokémon como anteriormente, mas usando a biblioteca requests, execute o seguinte código:

import requests
import time

start_time = time.time()

for number in range(1, 151):
    url = f'https://pokeapi.co/api/v2/pokemon/{number}'
    resp = requests.get(url)
    pokemon = resp.json()
    print(pokemon['name'])

print("--- %s seconds ---" % (time.time() - start_time))

Deve aparecer o mesmo resultado, mas com tempo de execução diferente:

Resultado de 150 chamadas de API usando solicitações

Com quase 29 segundos, é consideravelmente mais lento do que o código anterior. Para cada solicitação consecutiva, antes mesmo de começar o processo, é preciso aguardar a conclusão da etapa anterior. Isso leva muito mais tempo porque esse código aguarda a conclusão em sequência das 150 solicitações.

Como utilizar a asyncio para melhorar o desempenho

Se for feita a comparação de 8 segundos para 29 segundos, é um grande salto de desempenho, mas podemos fazer ainda melhor usando as ferramentas fornecidas pela asyncio. No exemplo original, é usado o await depois de cada solicitação HTTP, o que não é o ideal. Ele ainda é mais rápido do que o exemplo de requests porque tudo é executado em corrotinas, mas é possível executar todas essas solicitações "simultaneamente" como tarefas asyncio e verificar os resultados no final usando asyncio.ensure_future e asyncio.gather.

Se o código que faz a solicitação for dividido na função de sua própria corrotina, é possível criar uma lista de tarefas formada por futures para cada solicitação. Depois, é possível descompactar essa lista em uma chamada gather, que executa tudo junto. Quando é usado o await nesta chamada para asyncio.gather, recebemos um iterável para todos os futures que foram passados, mantendo sua ordem na lista. Dessa forma, é usado o await uma única vez.

Para visualizar o que acontece quando é implementamos, execute o seguinte código:

import aiohttp
import asyncio
import time

start_time = time.time()


async def get_pokemon(session, url):
    async with session.get(url) as resp:
        pokemon = await resp.json()
        return pokemon['name']


async def main():

    async with aiohttp.ClientSession() as session:

        tasks = []
        for number in range(1, 151):
            url = f'https://pokeapi.co/api/v2/pokemon/{number}'
            tasks.append(asyncio.ensure_future(get_pokemon(session, url)))

        original_pokemon = await asyncio.gather(*tasks)
        for pokemon in original_pokemon:
            print(pokemon)

asyncio.run(main())
print("--- %s seconds ---" % (time.time() - start_time))

Ele diminui o tempo a meros 1,53 segundos para as 150 solicitações HTTP! É um grande progresso, até mesmo em relação ao exemplo inicial de async/await. Este exemplo usa um código totalmente sem bloqueio, por isso o tempo total para executar as 150 solicitações será quase igual ao tempo de execução da solicitação mais demorada. Os números exatos variam conforme a conexão de Internet.

Resultado de 150 chamadas de API usando aiohttp e asyncio.gather

Considerações finais

Como pode se ver, o uso de bibliotecas como aiohttp para repensar o modo de fazer solicitações HTTP pode aumentar muito o desempenho do código e economizar bastante tempo se fizer muitas solicitações. Por padrão, ela é um pouco mais detalhada do que as bibliotecas síncronas, como requests, mas este comportamento é esperado, porque os desenvolvedores quiseram priorizar o desempenho.

Neste tutorial, mostramos apenas superficialmente o que é possível fazer com aiohttp e asyncio, mas esperamos ter facilitado um pouco o início da sua jornada no mundo do Python assíncrono.

Este artigo foi traduzido do original "Asynchronous HTTP Requests in Python with aiohttp and asyncio". Enquanto melhoramos nossos processos de tradução, adoraríamos receber seus comentários em help@twilio.com - contribuições valiosas podem render brindes da Twilio.