Heute veröffentlichen wir das zweite Material aus der Reihe, das sich mit der Verwendung von Python in Instagram befasst.
Beim letzten Mal wurden die Arten von Instagram-Servercode überprüft. Der Server ist ein in Python geschriebener Monolith. Es besteht aus mehreren Millionen Codezeilen und hat mehrere tausend Django-Endpunkte.

In diesem Artikel geht es darum, wie Instagram Typen verwendet, um HTTP-APIs zu dokumentieren und Verträge bei der Arbeit mit ihnen durchzusetzen.
Situationsübersicht
Wenn Sie den mobilen Instagram-Client öffnen, greift er über HTTP auf die JSON-API unseres Python-Servers (Django) zu.
Hier finden Sie einige Informationen zu unserem System, mit denen Sie sich ein Bild von der Komplexität der API machen können, mit der wir die Arbeit des mobilen Clients organisieren. Also hier ist was wir haben:
- Über 2000 Endpunkte auf dem Server.
- Über 200 Felder der obersten Ebene in einem Client-Datenobjekt, das ein Bild, ein Video oder eine Story in einer Anwendung darstellt.
- Hunderte von Programmierern, die Servercode schreiben (und noch mehr, die sich mit dem Client befassen).
- Täglich werden Hunderte von Commits für Servercode vorgenommen und die API geändert. Dies ist erforderlich, um neue Systemfunktionen zu unterstützen.
Wir verwenden Typen, um unsere komplexen, sich ständig weiterentwickelnden HTTP-APIs zu dokumentieren und Verträge bei der Arbeit mit ihnen durchzusetzen.
Typen
Beginnen wir von vorne. Die Beschreibung der Syntax für Typanmerkungen in Python-Code wurde in
PEP 484 veröffentlicht . Warum dem Code Typanmerkungen hinzufügen?
Betrachten Sie die Funktion, mit der Informationen über den Helden von Star Wars heruntergeladen werden:
def get_character(id, calendar): if id == 1000: return Character( id=1000, name="Luke Skywalker", birth_year="19BBY" if calendar == Calendar.BBY else ... ) ...
Um diese Funktion zu verstehen, müssen Sie den Code lesen. Nachdem Sie dies getan haben, können Sie Folgendes herausfinden:
- Es wird die Ganzzahlkennung (
id
) des Zeichens verwendet. - Es übernimmt den Wert aus der entsprechenden Aufzählung (
calendar
). Zum Beispiel steht Calendar.BBY
für "Vor der Schlacht von Yavin", dh "Vor der Schlacht von Yavin". - Es gibt Informationen über das Zeichen in Form einer Entität zurück, die Felder enthält, die die Kennung dieses Zeichens, seinen Namen und sein Geburtsjahr darstellen.
Die Funktion hat einen impliziten Vertrag, dessen Bedeutung der Programmierer jedes Mal wiederherstellen muss, wenn er den Funktionscode liest. Der Funktionscode wird jedoch nur einmal geschrieben, und Sie müssen ihn viele Male lesen, sodass dieser Ansatz für die Arbeit mit diesem Code nicht besonders gut ist.
Darüber hinaus ist es schwierig zu überprüfen, ob der Mechanismus, der die Funktion aufruft, dem oben beschriebenen impliziten Vertrag entspricht. Ebenso ist es schwierig zu überprüfen, ob dieser Vertrag im Hauptteil der Funktion eingehalten wird. In einer großen Codebasis können solche Situationen zu Fehlern führen.
Betrachten Sie nun dieselbe Funktion, die Typanmerkungen deklariert:
def get_character(id: int, calendar: Calendar) -> Character: ...
Mit Typanmerkungen können Sie den Vertrag dieser Funktion explizit ausdrücken. Um zu verstehen, was in eine Funktion eingegeben werden muss und was diese Funktion zurückgibt, lesen Sie einfach ihre Signatur. Ein Typprüfsystem kann die Funktion statisch analysieren und die Einhaltung des Vertrags im Code überprüfen. Auf diese Weise können Sie eine ganze Klasse von Fehlern beseitigen!
Typen für verschiedene HTTP-APIs
Wir werden eine HTTP-API entwickeln, mit der Sie Informationen über die Helden von Star Wars erhalten. Um den expliziten Vertrag zu beschreiben, der bei der Arbeit mit dieser API verwendet wird, verwenden wir Typanmerkungen.
Unsere API sollte die Zeichenkennung (
id
) als URL-Parameter und den Wert der
calendar
als Anforderungsparameter akzeptieren. Die API sollte eine JSON-Antwort mit Zeicheninformationen zurückgeben.
So sieht die API-Anforderung aus und welche Antwort wird zurückgegeben:
curl -X GET https://api.starwars.com/characters/1000?calendar=BBY { "id": 1000, "name": "Luke Skywalker", "birth_year": "19BBY" }
Um diese API in Django zu implementieren, müssen Sie zuerst den URL-Pfad und die Ansichtsfunktion registrieren, die für den Empfang der über diesen Pfad gestellten HTTP-Anforderung und die Rückgabe der Antwort verantwortlich sind.
urlpatterns = [ url("characters/<id>/", get_character) ]
Die Funktion akzeptiert als Eingabe die Anforderungs- und URL-Parameter (in unserem Fall
id
). Es analysiert und wandelt den
calendar
, der der Wert aus der entsprechenden Aufzählung ist, in den erforderlichen Typ um. Es lädt Zeichendaten aus dem Speicher und gibt ein in JSON serialisiertes und in eine HTTP-Antwort eingeschlossenes Wörterbuch zurück.
def get_character(request: IGWSGIRequest, id: str) -> JsonResponse: calendar = Calendar(request.GET.get("calendar", "BBY")) character = Store.get_character(id, calendar) return JsonResponse(asdict(character))
Obwohl die Funktion mit Typanmerkungen versehen ist, wird der feste Vertrag für die HTTP-API nicht explizit beschrieben. Aus der Signatur dieser Funktion können wir die Namen oder Typen von Anforderungsparametern oder Antwortfeldern und deren Typen nicht herausfinden.
Ist es möglich, dass die Signatur der Funktionsdarstellung genau so aussagekräftig ist wie die Signatur der zuvor betrachteten Funktion mit Typanmerkungen?
def get_character(id: int, calendar: Calendar) -> Character: ...
Funktionsparameter können Abfrageparameter sein (URL-, Abfrage- oder Abfragekörperparameter). Der von der Funktion zurückgegebene Werttyp kann den Inhalt der Antwort darstellen. Mit diesem Ansatz hätten wir einen expliziten und verständlichen Vertrag für die HTTP-API zur Verfügung, dessen Einhaltung durch ein Typprüfungssystem sichergestellt werden könnte.
Implementierung
Wie kann man diese Idee umsetzen?
Wir verwenden einen
Dekorateur , um eine stark typisierte Darstellungsfunktion in eine Django-Darstellungsfunktion umzuwandeln. Dieser Schritt erfordert keine Änderungen in Bezug auf die Arbeit mit dem Django-Framework. Wir können dieselbe Middleware, dieselben Routen und andere Komponenten verwenden, die wir gewohnt sind.
@api_view def get_character(id: int, calendar: Calendar) -> Character: ...
Betrachten Sie die Details der
api_view
von
api_view
decorator:
def api_view(view): @functools.wraps(view) def django_view(request, *args, **kwargs): params = { param_name: param.annotation(extract(request, param)) for param_name, param in inspect.signature(view).parameters.items() } data = view(**params) return JsonResponse(asdict(data)) return django_view
Dies ist ein schwer zu verstehender Code. Lassen Sie uns seine Funktionen analysieren.
Als Eingabewert nehmen wir eine stark typisierte Darstellungsfunktion und verpacken sie in eine reguläre Django-Darstellungsfunktion, die wir zurückgeben:
def api_view(view): @functools.wraps(view) def django_view(request, *args, **kwargs): ... return django_view
Schauen Sie sich nun die Implementierung der Django-Ansichtsfunktion an. Zuerst müssen wir Argumente für eine stark typisierte Präsentationsfunktion konstruieren. Wir verwenden Introspection und das
Inspect- Modul, um die Signatur dieser Funktion zu erhalten und ihre Parameter zu durchlaufen:
for param_name, param in inspect.signature(view).parameters.items()
Für jeden Parameter rufen wir die
extract
, die den Parameterwert aus der Anforderung extrahiert.
Dann wandeln wir den Parameter in den erwarteten Typ um, der in der Signatur angegeben ist (z. B. wandeln wir den Zeichenfolgenkalender in einen Wert um, der ein Element der
Calendar
).
param.annotation(extract(request, param))
Wir rufen eine stark typisierte Ansichtsfunktion mit den von uns konstruierten Argumenten auf:
data = view(**params)
Die Funktion gibt einen stark typisierten Wert der
Character
. Wir nehmen diesen Wert, wandeln ihn in ein Wörterbuch um und verpacken ihn in eine HTTP-Antwort im JSON-Format:
return JsonResponse(asdict(data))
Großartig! Wir haben jetzt eine Django-Ansichtsfunktion, die eine stark typisierte Ansichtsfunktion umschließt. Schauen Sie sich zum Schluss die
extract
:
def extract(request: HttpRequest, param: Parameter) -> Any: if request.resolver_match.route.contains(f"<{param}>"): return request.resolver_match.kwargs.get(param.name) else: return request.GET.get(param.name)
Jeder Parameter kann ein URL-Parameter oder ein Anforderungsparameter sein. Der Anforderungs-URL-Pfad (der Pfad, den wir zu Beginn registriert haben) ist im Routenobjekt des Django-URL-Locator-Systems verfügbar. Wir überprüfen den Parameternamen im Pfad. Wenn es einen Namen gibt, haben wir einen URL-Parameter. Dies bedeutet, dass wir es irgendwie aus der Anfrage extrahieren können. Andernfalls ist dies ein Abfrageparameter, den wir auch extrahieren können, jedoch auf andere Weise.
Das ist alles. Dies ist eine vereinfachte Implementierung, die jedoch die Grundidee der Eingabe einer API veranschaulicht.
Datentypen
Der Typ, der zur Darstellung des Inhalts der HTTP-Antwort verwendet wird (d. H.
Character
), kann entweder durch eine Datenklasse oder ein typisiertes Wörterbuch dargestellt werden.
Eine Datenklasse ist ein kompaktes Klassenbeschreibungsformat, das Daten darstellt.
from dataclasses import dataclass @dataclass(frozen=True) class Character: id: int name: str birth_year: str luke = Character( id=1000, name="Luke Skywalker", birth_year="19BBY" )
Instagram verwendet normalerweise Datenklassen, um HTTP-Antwortobjekte zu modellieren. Hier sind ihre Hauptmerkmale:
- Sie generieren automatisch Vorlagenkonstruktionen und verschiedene Hilfsmethoden.
- Sie sind für Typprüfungssysteme verständlich, dh Werte können einer Typprüfung unterzogen werden.
- Sie behalten ihre Immunität dank des Konstrukts
frozen=True
. - Sie sind in der Python-Standardbibliothek 3.7 oder als Backport im Python-Paketindex verfügbar.
Leider verfügt Instagram über eine veraltete Codebasis, die große, nicht typisierte Wörterbücher verwendet, die zwischen Funktionen und Modulen ausgetauscht werden. Es wäre nicht einfach, all diesen Code von Wörterbüchern in Datenklassen zu übersetzen. Infolgedessen verwenden wir Datenklassen für den neuen Code und in veraltetem Code
typisierte Wörterbücher .
Durch die Verwendung typisierter Wörterbücher können wir Client-Wörterbuchobjekten Typanmerkungen hinzufügen und, ohne das Verhalten eines funktionierenden Systems zu ändern, die Funktionen zur Typprüfung verwenden.
from mypy_extensions import TypedDict class Character(TypedDict): id: int name: str birth_year: str luke: Character = {"id": 1000} luke["name"] = "Luke Skywalker" luke["birth_year"] = 19
Fehlerbehandlung
Es wird erwartet, dass die Ansichtsfunktion Zeicheninformationen in Form einer Zeichenentität zurückgibt. Was sollen wir tun, wenn wir einen Fehler an den Client zurückgeben müssen?
Sie können eine Ausnahme auslösen, die vom Framework abgefangen und in eine HTTP-Antwort mit Fehlerinformationen konvertiert wird.
@api_view("GET") def get_character(id: str, calendar: Calendar) -> Character: try: return Store.get_character(id) except CharacterNotFound: raise Http404Exception()
Dieses Beispiel zeigt auch die HTTP-Methode im Decorator, mit der die für diese API zulässigen HTTP-Methoden festgelegt werden.
Die Werkzeuge
Die HTTP-API wird stark mithilfe der HTTP-Methode, der Anforderungstypen und der Antworttypen typisiert. Wir können diese API überprüfen und festlegen, dass eine GET-Anforderung mit der
id
Zeichenfolge im URL-Pfad und dem
calendar
für die entsprechende Aufzählung in der Abfragezeichenfolge akzeptiert werden soll. Wir können auch lernen, dass als Antwort auf eine solche Anfrage eine JSON-Antwort mit Informationen über die Art des
Character
.
Was kann mit all diesen Informationen gemacht werden?
OpenAPI ist ein API-Beschreibungsformat, auf dessen Grundlage eine
Vielzahl von Hilfstools erstellt wird. Dies ist ein ganzes Ökosystem. Wenn wir Code schreiben, um eine Endpunkt-Introspektion durchzuführen und OpenAPI-Spezifikationen basierend auf den empfangenen Daten zu generieren, bedeutet dies, dass wir über die Funktionen dieser Tools verfügen.
paths: /characters/{id}: get: parameters: - in: path name: id schema: type: integer required: true - in: query name: calendar schema: type: string enum: ["BBY"] responses: '200': content: application/json: schema: type: object ...
Wir können eine HTTP-API-Dokumentation für die
get_character
API
get_character
, die Namen, Typen, Anforderungs- und
get_character
enthält. Dies ist eine geeignete Abstraktionsebene für Cliententwickler, die Anforderungen an den entsprechenden Endpunkt erfüllen müssen. Sie müssen den Python-Implementierungscode für diesen Endpunkt nicht lesen.
API-DokumentationAuf dieser Basis können Sie zusätzliche Werkzeuge erstellen. Zum Beispiel ein Mittel zum Ausführen einer Anfrage von einem Browser. Auf diese Weise können Entwickler auf die für sie interessanten HTTP-APIs zugreifen, ohne Code schreiben zu müssen. Wir können sogar typsicheren Clientcode generieren, um sicherzustellen, dass Typen sowohl auf dem Client als auch auf dem Server ordnungsgemäß funktionieren. Aus diesem Grund verfügen wir möglicherweise über eine streng typisierte API auf dem Server, deren Aufrufe mit streng typisiertem Clientcode ausgeführt werden.
Darüber hinaus können wir ein Abwärtskompatibilitätsprüfungssystem erstellen. Was passiert, wenn wir eine neue Version des
birth_year
, in der wir für den Zugriff auf die betreffende API
id
,
name
und
birth_year
, und dann verstehen wir, dass wir nicht die Geburtstage aller Zeichen kennen? In diesem Fall muss der Parameterirth_year optional gemacht werden, aber alte Versionen von Clients, die einen ähnlichen Parameter erwarten, funktionieren möglicherweise einfach nicht mehr. Obwohl sich unsere APIs in der expliziten Typisierung unterscheiden, können sich die entsprechenden Typen ändern (z. B. ändert sich die API, wenn die Verwendung des Geburtsjahres des Charakters zuerst obligatorisch war und dann optional wurde). Wir können API-Änderungen verfolgen und API-Entwickler warnen, indem wir sie zum richtigen Zeitpunkt auffordern, dass sie durch einige Änderungen die Leistung von Clients beeinträchtigen können.
Zusammenfassung
Es gibt eine ganze Reihe von Anwendungsprotokollen, mit denen Computer miteinander kommunizieren können.
Eine Seite dieses Spektrums wird durch RPC-Frameworks wie Thrift und gRPC dargestellt. Sie unterscheiden sich darin, dass sie normalerweise strenge Typen für Anforderungen und Antworten festlegen und Client- und Servercode für die Organisation des Betriebs von Anforderungen generieren. Sie können ohne HTTP und sogar ohne JSON auskommen.
Andererseits gibt es in Python geschriebene unstrukturierte Webframeworks, die keine expliziten Verträge für Anforderungen und Antworten haben. Unser Ansatz bietet die Funktionen, die für klar strukturierte Frameworks charakteristisch sind, ermöglicht Ihnen jedoch gleichzeitig die weitere Verwendung des HTTP + JSON-Bundles und trägt dazu bei, dass Sie nur minimale Änderungen am Anwendungscode vornehmen müssen.
Es ist wichtig zu beachten, dass diese Idee nicht neu ist. Es gibt viele Frameworks, die in stark typisierten Sprachen geschrieben sind und Entwicklern die von uns beschriebenen Funktionen bieten. Wenn wir über Python sprechen, ist dies beispielsweise das
APIStar- Framework.
Wir haben die Verwendung von Typen für die HTTP-API erfolgreich in Auftrag gegeben. Wir konnten den beschriebenen Ansatz auf die Typisierung der API in unserer gesamten Codebasis anwenden, da sie gut auf vorhandene Präsentationsfunktionen anwendbar ist. Der Wert dessen, was wir getan haben, ist für alle unsere Programmierer offensichtlich. Wir sprechen nämlich von der Tatsache, dass die automatisch generierte Dokumentation zu einem effektiven Kommunikationsmittel für diejenigen geworden ist, die an der Entwicklung des Servers mit denen beteiligt sind, die den Instagram-Client schreiben.
Liebe Leser! Wie gehen Sie beim Entwurf von HTTP-APIs in Ihren Python-Projekten vor?
