© Grfxpro/Shutterstock.com
Pythons Web-Framework erhält neue Funktion

Django wird asynchron


Die erweiterte Async-Funktion hält Einzug in Pythons beliebtes Web-Framework Django. Doch welche Vorteile hat die Neuerung für Nutzer und wie können Async Views, Middleware und Tests verwendet werden?

Django – benannt nach dem Jazzgitarristen Django Reinhardt – ist eines der großen Web-Frameworks für die Programmiersprache Python. „The web framework for perfectionists with deadlines“, wie die offizielle Seite [1] verlauten lässt. Django verfolgt einen „batteries included“ Ansatz – im Gegensatz zu Flask – ähnlich Ruby on Rails. Die Lernkurve ist zwar steiler als bei Flask, dafür verkürzt sich die Entwicklungszeit der Applikationen merklich.

Im Vergleich zu Node.js-basierten Web-Frameworks war es in Django und Flask bisher deutlich schwerer, unterschiedliche Tasks gleichzeitig in Bearbeitung zu halten. Das wird sich in Zukunft ändern, denn auch Django hat jetzt die erweiterte asynchrone Funktionalität erhalten. Ab Django in der Version 3.1 (Release am 4. August 2020) werden asynchrone Views, Middle-wares und Tests unterstützt. Async-Unterstützung für weitere Funktionen (ORM, Formulare und Caching) wird zukünftig folgen. Wer die neuen Features nicht nutzen möchte, muss nichts ändern, der bestehende Code läuft weiter wie bisher. Mit Version 3.0 [2] kam das „asynchrone Server-Gateway-Interface“ ASGI [3], allerdings konnte es asynchron bisher kaum benutzt werden (mit Ausnahme von Fileuploads, denn die erreichen den View-Layer nicht, der in 3.0 noch nicht Async-fähig war). In welchen Fällen können Async Views am besten eingesetzt werden? Insbesondere, um viele kleine Tasks simultan auszuführen. Ein paar Beispiele:

  • Chat-Services wie Slack [4]

  • Ein neues API-Frontend für das Django REST Framework [5], das Änderungen der Daten auf den Endpunkten unmittelbar anzeigt

  • Dashboardanwendungen, die die aktuelle Anzahl aktiver Verbindungen oder Requests pro Sekunde in Echtzeit anzeigen

  • Gateway-APIs/Proxy Services

  • Onlinespiele (zum Beispiel MMOs wie EVE Online [6])

  • Anwendungen, die Phoenix LiveView [7] nutzen (weitere Beispiele auf Phoenix Phrenzy Results [8])

  • Ein reaktives Django-Admin-Interface [9], bei dem Änderungen der Model-Daten interaktiv angezeigt werden

Tom Christie stellte im Talk „Sketching out a Django redesign“ [10] auf der DjangoCon 2019 die Frage: „Müssen wir für solche Anwendungsfälle auf eine andere Sprache ausweichen?“ In letzter Zeit gewinnt Christies Projekt Starlette [11] zunehmend an Beliebtheit, vor allem in Kombination mit dem FastAPI Framework [12]. So ist es möglich, all das auch in Python zu realisieren. Aber wollen wir nicht bei Django bleiben und bestehende Django-Applikationen um die neuen Async-Features erweitern? Was dieser Artikel beinhaltet:

  1. Ein Beispiel, das zeigt, wie Async Views, Middleware und Tests verwendet werden

  2. Warum Async-Support überhaupt wichtig ist

  3. Besonderheiten, Geschichte und Details zu Async

Teil 1 – Beispiel: Async Views

Wer den Beispielcode selbst ausführen möchte, benötigt zunächst eine funktionierende Python-Installation [13]. Wir raten zu einem aktuellen Python 3.8, da bei dem Thema Async bei jeder neuen Version wesentliche Verbesserungen in puncto Stabilität und Usability hinzukommen. Funktionieren sollte aber jede Python-Version ab 3.6.

Die Entwicklungsumgebung

Für das Management der Abhängigkeiten in unseren Projekten verwenden wir heutzutage meist Poetry [14]. Um die Sache für dieses Beispiel nicht weiter zu verkomplizieren, verwenden wir hier der Einfachheit halber das eingebaute venv-Modul. Entweder benutzt ihr die Kommandos für eine POSIX-Shell (macOS, Linux usw.)

mkdir mysite && cd mysite python -m venv mysite_venv && source mysite_venv/bin/activate python -m pip install django==3.1 httpx 

oder die Windows PowerShell:

mkdir mysite && cd mysite python -m venv mysite_venv && . mysite_venv/Scripts/activate.ps1 python -m pip install django==3.1rc1 httpx 

Dann initialisieren wir Django:

django-admin startproject mysite . # create django project in current directory python manage.py migrate # migrate sqlite python manage.py runserver # start development server 

Nun sollte auf dem Localhost [15] die Django-Beispielseite zu sehen sein. Hinweis: Die Umgebungsvariable DJANGO_SETTINGS_MODULE darf nicht bereits gesetzt sein, sonst gibt es bereits beim migrate-Aufruf eine Fehlermeldung. Uns passiert das leider ständig.

Sync und Async Views

In Django nennt man das Beantworten einer Anfrage eine „View“. Eine View schickt auf einen Request eine Response. Wir beginnen mit einer normalen synchronen View, die eine einfache JSON Response zurückgibt. Genauso haben wir das auch schon in früheren Versionen von Django geschrieben. Der View wird ein optionaler Parameter (task_id) übergeben. Den wollen wir später nutzen, um den URL zu identifizieren, der aufgerufen wurde. Die View schläft zwischendurch absichtlich für eine Sekunde ein, um Anfragen zu emulieren, die erst nach einiger Zeit beantwortet werden. In Listing 1 erstellen wir mysite/views.py. Ebenso eine mysite/urls.py:

from django.urls import path from . import views urlpatterns = [ path("api/", views.api), ] 

Listing 1

import time from django.http import JsonResponse def api(request): time.sleep(1) payload = {"message": "Hello World!"} if "task_id" in request.GET: payload["task_id"] = request.GET["task_id"] return JsonResponse(payload) 

Die Antwort unserer kleinen API View solltet ihr nun unter http://localhost:8000/api/ im Browser sehen: bisher kein Unterschied zu einer normalen synchronen API View in Django vor 3.1. Im nächsten Schritt bauen wir eine Async View, die gleichzeitig zehn Anfragen an unsere erste synchrone View schickt und dann die Ergebnisse in einer neuen aggregierten Antwort kombiniert. mysite/views.py ergänzen wir um Listing 2. Auch die urls.py passen wir an:

urlpatterns = [ path("api/", views.api), path("api/aggregated/", views.api_aggregated), ] 

Listing 2

import httpx import asyncio def get_api_urls(num=10): base_url = "http://127.0.0.1:8000/api/" return [f"{base_url}?task_id={task_id}" for task_id in range(num)] async def api_aggregated(request): s = time.perf_counter() responses = [] urls = get_api_urls(num=10) async with httpx.AsyncClient() as client: responses = await asyncio.gather(*[client.get(url) for url in urls]) responses = [r.json() for r in responses] elapsed = time.perf_counter() - s result = { "message": "Hello Async World!", "responses": responses, "debug_message": f"fetch executed in {elapsed:0.2f} seconds.", } return JsonResponse(result) 

Unter http://localhost:8000/api/aggregated/ kann man nun zum ersten Mal eine Antwort sehen, die von einer echt asynchronen Funktion erzeugt wurde. In einer konventionellen synchronen View hätten wir httpx.get(url) für jeden URL in einer for-Schleife aufgerufen und anschließend jeweils eine Sekunde warten müssen. Denn letztlich wartet jede einzelne View eine Sekunde, bevor sie antwortet. Insgesamt hätte das Ganze mindestens zehn Sekunden gedauert. Da unsere Async View aber nur eine Sekunde für die Antwort gebraucht hat, kann sie nicht eine Anfrage nach der der anderen geschickt haben, sondern muss alle simultan gesendet und dann gleichzeitig auf alle Antworten gewartet haben. Die Antworten kamen schon nach einer Sekunde, weil die synchronen Views alle gleichzeitig gewartet haben.

Der Vergleich mit der Sync View

Wir testen unsere Hypothese, indem wir die aggregierte View in mysite/views.py synchron nachbauen (Listing 3).

Listing 3

def api_aggregated_sync(request): s = time.perf_counter() responses = [] urls = get_api_urls(num=10) for url in urls: r = httpx.get(url) responses.append(r.json()) elapsed = time.perf_counter() - s result = { "message": "Hello Sync World!", "aggregated_responses": responses, "debug_message": f"fetch executed in {elapsed:0.2f} seconds.", } return JsonResponse(result) 

Auch diese benötigt erneut eine Route:

urlpatterns = [ path("api/", views.api), path("api/aggregated/", views.api_aggregated), path("api/aggregated/sync/", views.api_aggregated_sync), ] 

Wie erwartet, braucht diese Sync-Aggregations-View nun ungefähr zehn Sekunden (http://localhost:8000/api/aggregated/sync/).

Aber warum hat unsere Async View überhaupt funktioniert? Haben wir nicht bloß den normalen Entwicklungsserver verwendet, den Django bereits mitgeliefert hat? Müssten wir nicht eigentlich einen ASGI-Server [3] verwenden? Wir haben unsere Aggregations-View mit async def annotiert. Daran kann Django erkennen, dass wir eine asynchrone View schreiben wollen. Und deshalb führt Django unsere View in einem eigens dafür erstellten Thread mit eigenem Event Loop [16] aus. Das ist praktisch, weil wir jetzt Async Views auch in normalen WSGI [17]-Applikationen verwenden können, ohne auf einen neuen Applikationsserver umsteigen zu müssen. Solange wir nur asynchron andere Endpunkte abfragen wollen, wie in unserem letzten Beispiel, reicht WSGI mit async def schon aus. Was wir mit Async Views innerhalb von WSGI-Applikationen allerdings nicht erreichen können, ist Anfragen von außen als Async-Task zu verarbeiten. Wir müssen immer noch für jede eingehende Anfrage einen neuen Thread erzeugen.

ASGI-Beispiel

Für ein Async-Beispiel, ...

Neugierig geworden?

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang