Der Weg zur Typprüfung von 4 Millionen Zeilen Python-Code. Teil 1

Heute machen wir Sie auf den ersten Teil der Übersetzung des Materials aufmerksam, in dem es darum geht, wie Dropbox mit der Typkontrolle von Python-Code umgeht.

Der Weg zur Typprüfung von 4 Millionen Zeilen Python-Code. Teil 1

Dropbox schreibt viel in Python. Es handelt sich um eine Sprache, die wir sehr häufig verwenden, sowohl für Back-End-Dienste als auch für Desktop-Client-Anwendungen. Wir verwenden auch häufig Go, TypeScript und Rust, aber Python ist unsere Hauptsprache. In Anbetracht unserer Größenordnung, bei der es sich um Millionen von Zeilen Python-Code handelt, stellte sich heraus, dass die dynamische Typisierung eines solchen Codes dessen Verständnis unnötig erschwerte und begann, die Arbeitsproduktivität ernsthaft zu beeinträchtigen. Um dieses Problem zu entschärfen, haben wir damit begonnen, unseren Code schrittweise auf die statische Typprüfung mit mypy umzustellen. Dies ist wahrscheinlich das beliebteste eigenständige Typprüfungssystem für Python. Mypy ist ein Open-Source-Projekt, seine Hauptentwickler arbeiten in Dropbox.

Dropbox war eines der ersten Unternehmen, das die statische Typprüfung in diesem Umfang in Python-Code implementierte. Mypy wird heutzutage in Tausenden von Projekten verwendet. Dieses Werkzeug wurde unzählige Male, wie man sagt, „im Kampf getestet“. Wir haben einen langen Weg zurückgelegt, um dorthin zu gelangen, wo wir jetzt sind. Auf dem Weg dorthin gab es viele erfolglose Unternehmungen und gescheiterte Experimente. Dieser Beitrag behandelt die Geschichte der statischen Typprüfung in Python, von ihren steinigen Anfängen im Rahmen meines Forschungsprojekts bis zur Gegenwart, als Typprüfung und Typhinweise für unzählige Entwickler, die in Python schreiben, alltäglich geworden sind. Diese Mechanismen werden mittlerweile von vielen Tools wie IDEs und Codeanalysatoren unterstützt.

Lesen Sie den zweiten Teil

Warum ist eine Typprüfung notwendig?

Wenn Sie jemals dynamisch typisiertes Python verwendet haben, sind Sie möglicherweise verwirrt darüber, warum es in letzter Zeit so viel Aufregung um statische Typisierung und Mypy gibt. Oder vielleicht gefällt Ihnen Python gerade wegen seiner dynamischen Eingabe, und das, was passiert, regt Sie einfach auf. Der Schlüssel zum Wert der statischen Typisierung liegt im Umfang der Lösungen: Je größer Ihr Projekt, desto mehr neigen Sie zur statischen Typisierung und desto mehr benötigen Sie sie letztendlich wirklich.

Angenommen, ein bestimmtes Projekt hat die Größe von Zehntausenden Zeilen erreicht und es stellt sich heraus, dass mehrere Programmierer daran arbeiten. Wenn wir uns ein ähnliches Projekt ansehen, können wir aufgrund unserer Erfahrung sagen, dass das Verständnis seines Codes der Schlüssel zur Aufrechterhaltung der Produktivität der Entwickler sein wird. Ohne Typanmerkungen kann es beispielsweise schwierig sein herauszufinden, welche Argumente an eine Funktion übergeben werden sollen oder welche Typen eine Funktion zurückgeben kann. Hier sind typische Fragen, die ohne die Verwendung von Typanmerkungen oft schwer zu beantworten sind:

  • Kann diese Funktion zurückkehren? None?
  • Was soll dieses Argument sein? items?
  • Was ist der Attributtyp? id: int ist es, str, oder vielleicht ein benutzerdefinierter Typ?
  • Sollte dieses Argument eine Liste sein? Ist es möglich, ein Tupel daran zu übergeben?

Wenn Sie sich den folgenden typannotierten Codeausschnitt ansehen und versuchen, ähnliche Fragen zu beantworten, stellt sich heraus, dass dies die einfachste Aufgabe ist:

class Resource:
    id: bytes
    ...
    def read_metadata(self, 
                      items: Sequence[str]) -> Dict[str, MetadataItem]:
        ...

  • read_metadata kehrt nicht zurück None, da der Rückgabetyp nicht vorhanden ist Optional[…].
  • Argument items ist eine Folge von Zeilen. Es kann nicht zufällig iteriert werden.
  • Attribut id ist eine Folge von Bytes.

In einer idealen Welt würde man erwarten, dass alle derartigen Feinheiten in der integrierten Dokumentation (Docstring) beschrieben würden. Die Erfahrung zeigt jedoch viele Beispiele dafür, dass eine solche Dokumentation im Code, mit dem Sie arbeiten müssen, häufig nicht beachtet wird. Auch wenn eine solche Dokumentation im Code vorhanden ist, kann man sich nicht auf deren absolute Korrektheit verlassen. Diese Dokumentation kann vage, ungenau und anfällig für Missverständnisse sein. In großen Teams oder großen Projekten kann dieses Problem äußerst akut werden.

Während sich Python in der frühen oder mittleren Phase von Projekten auszeichnet, stehen erfolgreiche Projekte und Unternehmen, die Python verwenden, irgendwann möglicherweise vor der entscheidenden Frage: „Sollten wir alles in einer statisch typisierten Sprache neu schreiben?“

Typprüfsysteme wie mypy lösen das oben genannte Problem, indem sie dem Entwickler eine formale Sprache zur Beschreibung von Typen zur Verfügung stellen und prüfen, ob Typdeklarationen mit der Programmimplementierung übereinstimmen (und optional ihre Existenz prüfen). Generell lässt sich sagen, dass uns diese Systeme so etwas wie eine sorgfältig geprüfte Dokumentation zur Verfügung stellen.

Der Einsatz solcher Systeme hat weitere Vorteile, die bereits völlig nicht trivial sind:

  • Das Typprüfsystem kann einige kleine (und nicht ganz so kleine) Fehler erkennen. Ein typisches Beispiel ist, wenn sie vergessen, einen Wert zu verarbeiten None oder eine andere besondere Bedingung.
  • Das Code-Refactoring wird erheblich vereinfacht, da das Typprüfsystem häufig sehr genau erkennt, welcher Code geändert werden muss. Gleichzeitig müssen wir bei Tests nicht auf eine 100-prozentige Codeabdeckung hoffen, was in der Regel ohnehin nicht realisierbar ist. Wir müssen nicht in die Tiefe des Stack-Trace eintauchen, um die Ursache des Problems herauszufinden.
  • Selbst bei großen Projekten kann mypy oft in Sekundenbruchteilen eine vollständige Typprüfung durchführen. Und die Durchführung von Tests dauert normalerweise mehrere zehn Sekunden oder sogar Minuten. Das Typprüfsystem gibt dem Programmierer sofortiges Feedback und ermöglicht es ihm, seine Arbeit schneller zu erledigen. Er muss keine fragilen und schwer zu wartenden Unit-Tests mehr schreiben, die reale Entitäten durch Mocks und Patches ersetzen, nur um schneller Code-Testergebnisse zu erhalten.

IDEs und Editoren wie PyCharm oder Visual Studio Code nutzen die Leistungsfähigkeit von Typanmerkungen, um Entwicklern Codevervollständigung, Fehlerhervorhebung und Unterstützung für häufig verwendete Sprachkonstrukte zu bieten. Und das sind nur einige der Vorteile des Tippens. Für manche Programmierer ist das alles das Hauptargument für das Tippen. Davon profitiert man unmittelbar nach der Umsetzung. Dieser Anwendungsfall für Typen erfordert kein separates Typprüfungssystem wie mypy, obwohl zu beachten ist, dass mypy dabei hilft, Typanmerkungen mit dem Code konsistent zu halten.

Hintergrund von mypy

Die Geschichte von mypy begann im Vereinigten Königreich, in Cambridge, einige Jahre bevor ich zu Dropbox kam. Im Rahmen meiner Doktorarbeit beschäftigte ich mich mit der Vereinheitlichung statisch typisierter und dynamischer Sprachen. Inspiriert wurde ich von einem Artikel über inkrementelles Tippen von Jeremy Siek und Walid Taha sowie vom Typed Racket-Projekt. Ich habe versucht, Wege zu finden, dieselbe Programmiersprache für verschiedene Projekte zu verwenden – von kleinen Skripten bis hin zu Codebasen, die aus vielen Millionen Zeilen bestehen. Gleichzeitig wollte ich sicherstellen, dass man bei einem Projekt jeder Größenordnung keine allzu großen Kompromisse eingehen muss. Ein wichtiger Teil davon war die Idee, schrittweise von einem untypisierten Prototypenprojekt zu einem umfassend getesteten statisch typisierten Endprodukt überzugehen. Heutzutage werden diese Ideen weitgehend als selbstverständlich angesehen, aber im Jahr 2010 war es ein Problem, das noch aktiv erforscht wurde.

Meine ursprüngliche Arbeit zur Typprüfung war nicht auf Python ausgerichtet. Stattdessen habe ich eine kleine „hausgemachte“ Sprache verwendet Alore. Hier ist ein Beispiel, das Ihnen verdeutlichen soll, wovon wir sprechen (Typanmerkungen sind hier optional):

def Fib(n as Int) as Int
  if n <= 1
    return n
  else
    return Fib(n - 1) + Fib(n - 2)
  end
end

Die Verwendung einer vereinfachten Muttersprache ist ein gängiger Ansatz in der wissenschaftlichen Forschung. Dies liegt nicht zuletzt daran, dass Experimente schnell durchgeführt werden können, und auch daran, dass das, was nichts mit der Studie zu tun hat, leicht ignoriert werden kann. Programmiersprachen in der realen Welt neigen dazu, große Phänomene mit komplexen Implementierungen zu sein, was das Experimentieren verlangsamt. Allerdings erscheinen alle Ergebnisse, die auf einer vereinfachten Sprache basieren, ein wenig verdächtig, da der Forscher bei der Erlangung dieser Ergebnisse möglicherweise wichtige Überlegungen für den praktischen Gebrauch von Sprachen geopfert hat.

Mein Typprüfer für Alore sah sehr vielversprechend aus, aber ich wollte ihn testen, indem ich mit echtem Code experimentierte, der, so könnte man sagen, nicht in Alore geschrieben war. Zum Glück basierte die Alore-Sprache größtenteils auf den gleichen Ideen wie Python. Es war einfach genug, die Typprüfung so umzugestalten, dass sie mit der Syntax und Semantik von Python funktioniert. Dadurch konnten wir die Typprüfung in Open-Source-Python-Code ausprobieren. Darüber hinaus habe ich einen Transpiler geschrieben, um in Alore geschriebenen Code in Python-Code zu konvertieren, und ihn zum Übersetzen meines Typechecker-Codes verwendet. Jetzt hatte ich ein in Python geschriebenes Typprüfsystem, das eine Teilmenge von Python unterstützte, eine Art dieser Sprache! (Bestimmte Architekturentscheidungen, die für Alore sinnvoll waren, waren für Python schlecht geeignet, und das macht sich in Teilen der Mypy-Codebasis immer noch bemerkbar.)

Tatsächlich konnte die von meinem Typsystem unterstützte Sprache zu diesem Zeitpunkt nicht ganz Python heißen: Es handelte sich aufgrund einiger Einschränkungen der Python 3-Typannotationssyntax um eine Variante von Python.

Es sah aus wie eine Mischung aus Java und Python:

int fib(int n):
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Eine meiner damaligen Ideen bestand darin, Typanmerkungen zu verwenden, um die Leistung zu verbessern, indem ich diese Art von Python in C- oder vielleicht JVM-Bytecode kompilierte. Ich war gerade dabei, einen Compiler-Prototyp zu schreiben, habe diese Idee jedoch aufgegeben, da die Typprüfung an sich recht nützlich schien.

Letztendlich habe ich mein Projekt auf der PyCon 2013 in Santa Clara vorgestellt. Darüber habe ich auch mit Guido van Rossum gesprochen, dem wohlwollenden Python-Diktator auf Lebenszeit. Er überzeugte mich, meine eigene Syntax aufzugeben und bei der Standard-Syntax von Python 3 zu bleiben. Python 3 unterstützt Funktionsanmerkungen, sodass mein Beispiel wie unten gezeigt umgeschrieben werden konnte, was zu einem normalen Python-Programm führte:

def fib(n: int) -> int:
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Ich musste einige Kompromisse eingehen (zuallererst möchte ich anmerken, dass ich genau aus diesem Grund meine eigene Syntax erfunden habe). Insbesondere Python 3.3, die damals aktuellste Version der Sprache, unterstützte keine Variablenanmerkungen. Ich habe mit Guido per E-Mail verschiedene Möglichkeiten zur syntaktischen Gestaltung solcher Annotationen besprochen. Wir haben uns entschieden, Typkommentare für Variablen zu verwenden. Dies erfüllte den beabsichtigten Zweck, war aber etwas umständlich (Python 3.6 gab uns eine schönere Syntax):

products = []  # type: List[str]  # Eww

Typkommentare erwiesen sich auch als nützlich für die Unterstützung von Python 2, das über keine integrierte Unterstützung für Typanmerkungen verfügt:

f fib(n):
    # type: (int) -> int
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Es stellte sich heraus, dass diese (und andere) Kompromisse eigentlich keine Rolle spielten – die Vorteile der statischen Typisierung führten dazu, dass Benutzer bald die nicht ideale Syntax vergaßen. Da im typgeprüften Python-Code keine speziellen syntaktischen Konstrukte verwendet wurden, funktionierten bestehende Python-Tools und Codeverarbeitungsprozesse weiterhin normal, was es für Entwickler viel einfacher machte, das neue Tool zu erlernen.

Guido überzeugte mich auch, nach Abschluss meiner Abschlussarbeit bei Dropbox einzusteigen. Hier beginnt der interessanteste Teil der Mypy-Geschichte.

To be continued ...

Liebe Leser! Wenn Sie Python verwenden, teilen Sie uns bitte den Umfang der Projekte mit, die Sie in dieser Sprache entwickeln.

Der Weg zur Typprüfung von 4 Millionen Zeilen Python-Code. Teil 1
Der Weg zur Typprüfung von 4 Millionen Zeilen Python-Code. Teil 1

Source: habr.com

Kommentar hinzufügen