Asynchrone HTTP-Anfragen in Python mit HTTPX und asyncio

August 11, 2021
Autor:in:
Sam Agnew
Twilion

Asynchrone HTTP-Anfragen in Python mit HTTPX und asyncio


Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Asynchronous HTTP Requests in Python with HTTPX and asyncio. Während wir unsere Übersetzungsprozesse verbessern, würden wir uns über Dein Feedback an help@twilio.com freuen, solltest Du etwas bemerken, was falsch übersetzt wurde. Wir bedanken uns für hilfreiche Beiträge mit Twilio Swag :)

Asynchroner Code ist zunehmend zu einer tragenden Säule der Python-Entwicklung geworden. Mit asyncio als Teil der Standardbibliothek und vielen Paketen von Drittanbietern, die damit kompatible Funktionen bieten, wird dieses Paradigma nicht so schnell verschwinden.

Wir wollen uns anschauen, wie man die HTTPX-Bibliothek verwendet, um dies für asynchrone HTTP-Anfragen zu nutzen. Das ist einer der häufigsten Anwendungsfälle für nicht blockierenden Code.

Was ist nicht blockierender Code?

Möglicherweise hörst du Begriffe wie „asynchron“, „nicht blockierend“ oder „gleichzeitig“ und bist ein wenig verwirrt darüber, was sie alle bedeuten. Diesem viel detaillierteren Tutorial zufolge gibt es zwei primäre Eigenschaften:

  • Asynchrone Routinen können pausieren, während sie auf ihr endgültiges Ergebnis warten, damit andere Routinen in der Zwischenzeit ausgeführt werden können.
  • Asynchroner Code erleichtert durch den oben genannten Mechanismus die gleichzeitige Ausführung. Anders ausgedrückt: Asynchroner Code vermittelt das Erscheinungsbild von Parallelität.

Asynchroner Code ist also Code, der hängen bleiben kann, während auf ein Ergebnis gewartet wird, damit in der Zwischenzeit anderer Code ausgeführt werden kann. Er „blockiert“ keinen anderen Code, so dass wir ihn „nicht blockierenden“ Code nennen können.

Die asyncio-Bibliothek bietet Python-Entwicklern eine Vielzahl von Tools, um dies zu tun, und aiohttp bietet eine noch spezifischere Funktionalität für HTTP-Anforderungen. HTTP-Anforderungen sind ein klassisches Beispiel für etwas, das sich gut für Asynchronität eignet, da sie das Warten auf eine Antwort von einem Server beinhalten. Während dieser Zeit wäre es bequem und effizient, anderen Code auszuführen.

Einrichtung

Stelle sicher, dass deine Python-Umgebung eingerichtet ist, bevor du beginnst. Folge dieser Anleitung bis zum Abschnitt virtualenv, wenn du Hilfe benötigst. Es ist wichtig, dass alles richtig funktioniert, besonders in Bezug auf virtuelle Umgebungen, um deine Abhängigkeiten zu isolieren, wenn mehrere Projekte auf demselben Computer ausgeführt werden. Du benötigst mindestens Python 3.7 oder höher, um den Code in diesem Beitrag ausführen zu können.

Nachdem deine Umgebung eingerichtet ist, musst du die HTTPX-Bibliothek zum Stellen sowohl asynchroner als auch synchroner Anforderungen installieren, die wir vergleichen werden. Installiere die Bibliothek mit dem folgenden Befehl, nachdem du deine virtuelle Umgebung aktiviert hast:

pip install httpx==0.18.2

Damit solltest du bereit sein, fortzufahren und Code zu schreiben.

Stellen einer HTTP-Anfrage mit HTTPX

Beginnen wir mit einer einzelnen GET-Anfrage mit HTTPX, um zu demonstrieren, wie die Schlüsselwörter async und await funktionieren. Wir werden das Pokemon API als Beispiel benutzen. Lasst uns zunächst versuchen, die Daten des legendären 151. Pokemon, Mew, zu erhalten.

Führe den folgenden Python-Code aus, und auf dem Terminal sollte der Name „mew“ angezeigt werden:

import asyncio
import httpx


async def main():
    pokemon_url = 'https://pokeapi.co/api/v2/pokemon/151'

    async with httpx.AsyncClient() as client:

        resp = await client.get(pokemon_url)

        pokemon = resp.json()
        print(pokemon['name'])

asyncio.run(main())

In diesem Code erstellen wir eine Coroutine namens „main“, die wir mit dem asyncio event loop laufen lassen. Hier stellen wir eine Anfrage an die Pokemon-API und warten dann auf eine Antwort.

Dieses Schlüsselwort async teilt dem Python-Interpreter grundsätzlich mit, dass die von uns definierte Coroutine asynchron mit einer Ereignisschleife ausgeführt werden soll. Das Schlüsselwort await gibt die Steuerung an die Ereignisschleife zurück, unterbricht die Ausführung der umgebenden Coroutine und lässt die Ereignisschleife andere Dinge ausführen, bis das Ergebnis „awaited“ zurückgegeben wird.

Stellen einer großen Anzahl von Anfragen.

Das Stellen einer einzelnen asynchronen HTTP-Anforderung ist großartig, da die Ereignisschleife bei anderen Aufgaben ausgeführt werden kann, anstatt den gesamten Thread zu blockieren, während auf eine Antwort gewartet wird. Diese Funktionalität ist wirklich hervorragend, wenn du versuchst, eine größere Anzahl von Anfragen zu stellen. Lasst uns dies demonstrieren, indem wir dieselbe Anforderung wie zuvor ausführen, jedoch für alle 150 der ursprünglichen Pokémon.

Nehmen wir den vorherigen Anforderungscode und fügen ihn in eine Schleife ein. Dabei werden die angeforderten und verwendeten Pokemon-Daten aktualisiert und await für jede Anfrage verwendet:

import asyncio
import httpx
import time

start_time = time.time()


async def main():

    async with httpx.AsyncClient() as client:

        for number in range(1, 151):
            pokemon_url = f'https://pokeapi.co/api/v2/pokemon/{number}'

            resp = await client.get(pokemon_url)
            pokemon = resp.json()
            print(pokemon['name'])

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

Dieses Mal messen wir auch, wie viel Zeit der gesamte Prozess benötigt. Wenn du diesen Code in deiner Python-Shell ausführst, sollte auf deinem Terminal Folgendes angezeigt werden:

Konsolenausgabe von 150 asynchronen Anfragen

8,6 Sekunden scheinen für 150 Anfragen ziemlich gut zu sein, aber wir haben nichts, mit dem wir es vergleichen könnten. Versuchen wir, dasselbe synchron zu erreichen.

Vergleichen der Geschwindigkeit mit synchronen Anforderungen

Führe den folgenden Code aus, um die ersten 150 Pokemon wie zuvor zu drucken, jedoch ohne async/await:

import httpx
import time

start_time = time.time()
client = httpx.Client()

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

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

Du solltest dieselbe Ausgabe mit einer anderen Laufzeit sehen:

Konsolenausgabe von 150 synchronen Anfragen mit einer Zeit von ~10 Sekunden

Die Geschwindigkeit scheint jedoch nicht viel niedriger zu sein als zuvor. Das liegt wahrscheinlich daran, dass das Verbindungspooling des HTTPX-Clients den größten Teil der Arbeit übernimmt. Wir können jedoch mehr asyncio-Funktionen nutzen, um eine bessere Leistung zu erzielen.

Verwendung von asyncio für eine verbesserte Leistung

asyncio bietet weitere Tools, die unsere Leistung insgesamt deutlich verbessern können. Im ursprünglichen Beispiel verwenden wir await nach jeder einzelnen HTTP-Anfrage, was nicht ganz ideal ist. Stattdessen können wir alle diese Anfragen „gleichzeitig“ als asyncio-Tasks ausführen und dann die Ergebnisse am Ende mit asyncio.ensure_future und asyncio.gather überprüfen.

Wenn der Code, der die Anfrage tatsächlich stellt, in eine eigene Coroutine-Funktion aufgeteilt wird, können wir eine Liste von Aufgaben erstellen, die aus Futures für jede Anfrage besteht. Wir können diese Liste dann in einem gather-Aufruf entpacken, der sie alle zusammen ausführt. Wenn wir dann await für asyncio.gather verwenden, erhalten wir eine Iterable für alle übergebenen Futures, wobei die Reihenfolge in der Liste beibehalten wird. Auf diese Weise verwenden wir await nur einmal.

Führe den folgenden Code aus, um zu sehen, was passiert, wenn wir dies implementieren:

import asyncio
import httpx
import time


start_time = time.time()


async def get_pokemon(client, url):
        resp = await client.get(url)
        pokemon = resp.json()

        return pokemon['name']


async def main():

    async with httpx.AsyncClient() as client:

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

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

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

Konsolenausgabe von 150 asynchronen Anfragen, jedoch mit einer deutlich schnelleren Laufzeit von 1,54 Sekunden

Dies reduziert unsere Zeit auf nur 1,54 Sekunden für 150 HTTP-Anfragen! Das ist eine enorme Verbesserung gegenüber den vorherigen Beispielen. Dies ist vollständig nicht blockierend, sodass die Gesamtzeit für die Ausführung aller 150 Anfragen in etwa der Zeit entspricht, die die längste Anfrage für die Ausführung benötigt hat. Die genauen Zahlen variieren je nach Internetverbindung.

Abschließende Gedanken

Wie du siehst, kann die Verwendung von Bibliotheken wie HTTPX zum Überdenken der Art und Weise, wie du HTTP-Anforderungen stellst, deinem Code eine enorme Leistungssteigerung verleihen und viel Zeit sparen, wenn du eine große Anzahl von Anfragen stellst.

In diesem Tutorial haben wir nur ansatzweise erläutert, welche Möglichkeiten du mit asyncio hast, aber ich hoffe, dass dir damit der Einstieg in die Welt des asynchronen Python ein wenig erleichtert wurde. Wenn du an einer anderen ähnlichen Bibliothek für asynchrone HTTP-Anfragen interessiert bist, empfehle ich diesen anderen Blogbeitrag, den ich über aiohttp geschrieben habe.

Ich bin gespannt auf eure Ergebnisse. Ihr könnt mich gerne kontaktieren, um eure Erfahrungen zu teilen oder Fragen zu stellen.