Asynchrone HTTP-Anforderungen in Python mit aiohttp und asyncio

June 22, 2021
Autor:in:
Sam Agnew
Twilion

Asynchrone HTTP-Anforderungen in Python mit aiohttp und asyncio


Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Asynchronous HTTP Requests in Python with aiohttp 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 aiohttp-Bibliothek verwendet, um dies für asynchrone HTTP-Anforderungen zu nutzen. Dies 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 sind zwei der primären 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 einige Bibliotheken von Drittanbietern installieren. Wir werden aiohttp für asynchrone Anfragen und die Anfragen-Bibliothek zum Erstellen regelmäßiger synchroner HTTP-Anforderungen verwenden, um die beiden später zu vergleichen. Installiere beide mit dem folgenden Befehl, nachdem du deine virtuelle Umgebung aktiviert hast:

pip install aiohttp-3.7.4.post0 requests==2.25.1

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

Stellen einer HTTP-Anfrage mit aiohttp

Beginnen wir mit einer einzelnen GET-Anfrage mit aiohttp, 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 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())

In diesem Code erstellen wir eine Coroutine namens main, die wir mit dem asyncio event loop laufen lassen. Hier öffnen wir eine aiohttp client session, ein einzelnes Objekt, das für eine ganze Reihe von Einzelanforderungen verwendet werden kann und standardmäßig Verbindungen mit bis zu 100 verschiedenen Servern gleichzeitig herstellen kann. In dieser Sitzung 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 jedoch wirklich hervorragend, wenn du versuchst, eine größere Anzahl von Anforderungen 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 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))

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:

Ergebnis von 150 API-Anfragen mit aiohttp

8 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 mithilfe der Requests-Bibliothek zu erreichen.

Vergleichen der Geschwindigkeit mit synchronen Anforderungen

Requests wurde als HTTP-Bibliothek „für Menschen“ entwickelt und verfügt daher über eine sehr schöne und vereinfachte API. Ich kann es sehr für Projekte empfehlen, bei denen Geschwindigkeit im Vergleich zu Entwicklerfreundlichkeit und einfach zu befolgendem Code möglicherweise nicht von größter Bedeutung ist.

Führe den folgenden Code aus, um die ersten 150 Pokemon wie zuvor zu drucken, aber die Requests-Bibliothek zu verwenden:

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))

Du solltest dieselbe Ausgabe mit einer anderen Laufzeit sehen:

Ergebnis von 150 API-Anfragen mit requests

Mit fast 29 Sekunden ist dies deutlich langsamer als der vorherige Code. Für jede aufeinanderfolgende Anfrage müssen wir warten, bis der vorherige Schritt abgeschlossen ist, bevor wir überhaupt mit dem Prozess beginnen. Es dauert viel länger, da dieser Code darauf wartet, dass 150 Anforderungen nacheinander abgeschlossen werden.

Verwendung von asyncio für eine verbesserte Leistung

8 Sekunden im Vergleich zu 29 Sekunden sind also ein enormer Leistungssprung, aber mit den Tools, die Asyncio bietet, können wir noch bessere Ergebnisse erzielen. Im ursprünglichen Beispiel verwenden wir await nach jeder einzelnen HTTP-Anfrage, was nicht ganz ideal ist. Es ist immer noch schneller als das Requests-Beispiel, da wir alles in Coroutinen ausführen. Stattdessen können wir alle diese Anforderungen „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 Anforderung 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 zurück, wobei die Reihenfolge in der Liste beibehalten wird. Auf diese Weise warten wir nur einmal.

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

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))

Dies reduziert unsere Zeit auf nur 1,53 Sekunden für 150 HTTP-Anfragen! Dies ist eine enorme Verbesserung gegenüber unserem ursprünglichen Beispiel für async/await. Dieses Beispiel ist vollständig nicht blockierend, sodass die Gesamtzeit für die Ausführung aller 150 Anforderungen in etwa der Zeit entspricht, die die längste Anforderung für die Ausführung benötigt hat. Die genauen Zahlen variieren je nach Internetverbindung.

Ergebnis von 150 API-Anfragen mit aiohttp und asyncio.gather

Abschließende Gedanken

Wie du siehst, kann die Verwendung von Bibliotheken wie aiohttp 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 Anforderungen stellst. Standardmäßig ist es etwas ausführlicher als synchrone Bibliotheken wie Requests, aber das ist beabsichtigt, da die Entwickler Leistung zu einer Priorität machen wollten.

In diesem Tutorial haben wir nur im Ansatz behandelt, was du mit aiohttp und asyncio tun kannst, aber ich hoffe, dass dies deine Reise in die Welt des asynchronen Python ein wenig erleichtert hat.

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