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

Heute präsentieren wir Ihnen den ersten Teil der Übersetzung des Materials über die Typüberprüfung von Python-Code bei Dropbox.

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

Bei Dropbox wird viel in Python programmiert. Das ist die Programmiersprache, die wir extrem weit verbreitet einsetzen — sowohl für Backend-Services als auch für Desktop-Client-Anwendungen. Darüber hinaus verwenden wir auch Go, TypeScript und Rust in großem Umfang, aber Python ist unsere Hauptsprache. Angesichts unserer Größenordnung, die Millionen von Zeilen Python-Code umfasst, stellte sich heraus, dass die dynamische Typisierung des Codes die Verständlichkeit unnötig komplizierte und ernsthaft die Produktivität beeinflusste. Um dieses Problem zu mildern, haben wir begonnen, unseren Code schrittweise auf statische Typüberprüfung mit mypy umzustellen. Dies ist wahrscheinlich das beliebteste eigenständige Typüberprüfungssystem für Python. Mypy ist ein Open-Source-Projekt, dessen Hauptentwickler bei Dropbox tätig sind.

Dropbox war eines der ersten Unternehmen, das statische Typüberprüfung in Python-Code in einem ähnlichen Maßstab implementierte. Heutzutage wird mypy in Tausenden von Projekten eingesetzt. Dieses Werkzeug hat sich unzählige Male, sozusagen, "im Einsatz bewährt". Um dorthin zu gelangen, wo wir heute stehen, mussten wir einen langen Weg zurücklegen. Auf diesem Weg gab es viele gescheiterte Versuche und Missgeschicke. Dieser Text erzählt die Geschichte der statischen Typüberprüfung in Python – vom schwierigen Anfang, der Teil meines Forschungsprojekts war, bis heute, wo Typüberprüfungen und Typvorschläge für unzählige Entwickler, die in Python programmieren, alltäglich geworden sind. Diese Mechanismen werden nun von zahlreichen Werkzeugen unterstützt – wie IDEs und Code-Analyzer.

Zweite Teil lesen

Warum ist Typüberprüfung wichtig?

Wenn Sie jemals mit dem dynamisch typisierten Python gearbeitet haben, fragen Sie sich vielleicht, warum rund um die statische Typisierung und mypy in letzter Zeit so viel Aufsehen gemacht wird. Möglicherweise lieben Sie Python gerade wegen seiner dynamischen Typisierung, und das alles verunsichert Sie einfach. Der Schlüssel zum Wert der statischen Typisierung liegt in der Größe der Projekte: Je größer Ihr Projekt ist, desto mehr neigen Sie zur statischen Typisierung, und schließlich wird sie für Sie unerlässlich.

Nehmen wir an, ein Projekt hat Größenordnungen von Zehntausenden von Zeilen erreicht, und es stellen sich mehrere Programmierer dem Projekt. Basierend auf unserer Erfahrung können wir bei einem solchen Projekt sagen, dass das Verständnis des Codes der Schlüssel zur Aufrechterhaltung der Produktivität der Entwickler sein wird. Ohne Typannotationen ist es oft schwierig zu klären, welche Argumente an eine Funktion übergeben werden müssen oder welche Rückgabetypen eine bestimmte Funktion liefern kann. Hier sind typische Fragen, auf die man häufig ohne Typannotationen nur schwer Antworten findet:

  • Kann diese Funktion zurückgeben None?
  • Was sollte dieses Argument sein? Elemente?
  • Was ist der Typ des Attributs? id: int ist es, str, oder vielleicht ein benutzerdefinierter Typ?
  • Sollte dieses Argument eine Liste sein? Kann man ein Tupel übergeben?

Wenn man sich den folgenden Codeabschnitt mit Typannotationen ansieht und versucht, solche Fragen zu beantworten, wird klar, dass dies eine einfache Aufgabe ist:

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

  • read_metadata gibt nicht zurück None, da der Rückgabewert nicht Optional[…].
  • Das Argument Elemente — eine Folge von Zeichenfolgen ist. Sie kann nicht in beliebiger Reihenfolge durchlaufen werden.
  • Das Attribut id — ist eine Bytezeichenfolge.

In einer idealen Welt könnte man erwarten, dass all diese Feinheiten in der integrierten Dokumentation (Docstring) festgehalten sind. Doch die Erfahrungswerte zeigen zahlreiche Beispiele dafür, dass es in dem Code, mit dem man arbeitet, oft an dieser Dokumentation mangelt. Selbst wenn eine solche Dokumentation vorhanden ist, darf man nicht auf ihre absolute Richtigkeit hoffen. Diese Dokumentation kann unklar, ungenau sein und viel Raum für Missverständnisse lassen. In großen Teams oder Projekten kann dieses Problem besonders gravierend werden.

Obwohl Python in den frühen oder mittleren Phasen von Projekten hervorragend abschneidet, kann es sein, dass erfolgreiche Projekte und Unternehmen, die Python nutzen, eines Tages vor der entscheidenden Frage stehen: „Müssen wir alles auf einer statisch typisierten Sprache neu schreiben?“

Typprüfungsysteme wie mypy lösen dieses Problem, indem sie Entwicklern eine formale Sprache zur Beschreibung von Typen bereitstellen und sicherstellen, dass die Typbeschreibungen mit der Implementierung der Programme übereinstimmen (und optional deren Existenz überprüfen). Im Großen und Ganzen kann man sagen, dass diese Systeme etwas wie sorgfältig geprüfte Dokumentation bieten.

Der Einsatz solcher Systeme bietet weitere Vorteile, die alles andere als trivial sind:

  • Das Typprüfungsystem kann einige kleinere (aber auch nicht unwesentliche) Fehler erkennen. Ein typisches Beispiel ist das Vergessen, einen Wert zu verarbeiten, None oder eine andere besondere Bedingung.
  • Die Refaktorisierung des Codes wird erheblich vereinfacht, da das Typprüfungsystem oft sehr genau angibt, welcher Code geändert werden muss. Dabei müssen wir nicht auf eine 100%ige Testabdeckung des Codes hoffen, was in jedem Fall normalerweise unerreichbar ist. Wir müssen nicht die Tiefen der Stack-Trace-Berichte durchforsten, um die Ursache des Problems herauszufinden.
  • Selbst bei großen Projekten kann mypy häufig eine vollständige Typprüfung in Bruchteilen von Sekunden durchführen. Die Ausführung von Tests dauert in der Regel jedoch Dutzende von Sekunden oder sogar Minuten. Das Typsystem bietet Programmierern sofortiges Feedback und ermöglicht es ihnen, ihre Arbeit schneller zu erledigen. Es ist nicht mehr nötig, fragiles und wartungsintensives Modultests zu schreiben, die reale Entitäten durch Mocks und Patches ersetzen, nur um schneller zu Ergebnissen zu kommen.

IDEs und Editoren wie PyCharm oder Visual Studio Code nutzen die Möglichkeiten der Typannotationen, um Entwicklern Funktionen wie die automatische Codevervollständigung, die Hervorhebung von Fehlern und die Unterstützung häufig verwendeter Sprachkonstruktionen anzubieten. Und das sind nur einige der Vorteile, die Typisierung mit sich bringt. Für einige Programmierer ist das das Hauptargument für Typisierung. Es bringt sofortige Vorteile nach der Implementierung in die Arbeit. Diese Art der Typnutzung erfordert nicht den Einsatz eines separaten Typsystems wie mypy, obwohl zu beachten ist, dass mypy hilft, die Konsistenz zwischen Typannotationen und Code aufrechtzuerhalten.

Die Vorgeschichte von mypy

Die Geschichte von mypy begann in Großbritannien, 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 durch einen Artikel über inkrementelle Typisierung von Jeremy Siek und Walid Taha sowie durch das Projekt Typed Racket. Ich versuchte, Wege zu finden, um dieselte Programmiersprache für unterschiedliche Projekte zu nutzen – von kleinen Skripten bis hin zu Codebasen mit mehreren Millionen Zeilen. Dabei war es mir wichtig, dass es in Projekten jeder Größe nicht zu großen Kompromissen kommen muss. Ein zentraler Punkt war die Idee, schrittweise von einem untypisierten Prototyp zu einem umfassend getesteten, statisch typisierten Endprodukt überzugehen. Heutzutage werden diese Ideen weitgehend als selbstverständlich angesehen, doch 2010 war dies ein Thema, das noch aktiv erforscht wurde.

Meine ursprüngliche Arbeit im Bereich der Typüberprüfung war nicht auf Python ausgerichtet. Stattdessen verwendete ich eine kleine "hausgemachte" Sprache. AloreHier ist ein Beispiel, das Ihnen helfen wird, zu verstehen, worum es geht (Typannotationen sind hier optional):

def Fib(n als Int) als Int
  wenn n <= 1
    return n
  sonst
    return Fib(n - 1) + Fib(n - 2)
  end
end

Die Verwendung einer vereinfachten, eigens entwickelten Sprache ist ein gängiger Ansatz in der wissenschaftlichen Forschung. Dies liegt nicht zuletzt daran, dass es das schnelle Durchführen von Experimenten ermöglicht und auch daran, dass alles, was nichts mit der Forschung zu tun hat, problemlos ignoriert werden kann. Tatsächlich sind Programmiersprachen meist komplexe Phänomene mit anspruchsvollen Implementierungen, was die Experimente verlangsamt. Allerdings wirken alle Ergebnisse, die auf einer vereinfachten Sprache basieren, etwas verdächtig, da der Forscher möglicherweise wichtige Überlegungen aufgegeben hat, die für die praktische Nutzung von Sprachen wesentlich sind.

Mein Typüberprüfungswerkzeug für Alore schien vielversprechend zu sein, aber ich wollte es testen, indem ich Experimente mit echtem Code durchführte, der, ehrlich gesagt, in Alore nicht verfügbar war. Glücklicherweise basierte die Sprache Alore weitgehend auf denselben Ideen wie Python. Daher war es relativ einfach, das Typüberprüfungswerkzeug so umzuprogrammieren, dass es mit der Syntax und Semantik von Python arbeiten konnte. So konnte ich Typüberprüfungen im Open-Source-Python-Code durchführen. Außerdem schrieb ich einen Transpiler, um Code, der in Alore geschrieben wurde, in Python-Code zu konvertieren und verwendete ihn, um den Code meines Typüberprüfungswerkzeugs zu übertragen. Jetzt hatte ich ein Typüberprüfungssystem, das in Python geschrieben war und ein Teilmenge von Python unterstützte – eine Art Variante dieser Sprache! (Bestimmte architektonische Entscheidungen, die für Alore sinnvoll waren, passten nicht gut zu Python, was immer noch in einigen Teilen der mypy-Codebasis bemerkbar ist.)

Tatsächlich konnte man die von meinem Typsystem unterstützte Sprache in diesem Moment nicht wirklich Python nennen: es war eine Variante von Python aufgrund bestimmter Einschränkungen der Typannotationen-Syntax von Python 3.

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 Ideen zu dieser Zeit war es, Typannotationen zu verwenden, um die Leistung durch die Kompilierung dieser Python-Variante nach C oder möglicherweise zu JVM-Bytecode zu verbessern. Ich kam so weit, einen Prototyp-Compiler zu schreiben, gab jedoch auf, da die Typüberprüfung selbst schon ziemlich nützlich erschien.

Letztendlich stellte ich mein Projekt auf der PyCon 2013 in Santa Clara vor. Ich sprach auch darüber mit Guido van Rossum, dem großzügigen lebenslangen Diktator von Python. Er überzeugte mich, auf meine eigene Syntax zu verzichten und der standardmäßigen Python 3-Syntax zu folgen. Python 3 unterstützt Funktionsannotationen, sodass ich mein Beispiel wie folgt umschreiben konnte und eine normale Python-Anwendung erhielt:

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

Ich musste einige Kompromisse eingehen (vor allem möchte ich hervorheben, dass ich deshalb meine eigene Syntax erfunden habe). Insbesondere unterstützte Python 3.3, die zu diesem Zeitpunkt neueste Version der Sprache, keine Variablenannotationen. Ich habe mit Guido per E-Mail verschiedene Möglichkeiten der syntaktischen Darstellung solcher Anmerkungen besprochen. Wir entschieden uns, für Variablen Kommentare mit Typangaben zu verwenden. Das erreichte zwar das gewünschte Ziel, sah aber etwas unhandlich aus (Python 3.6 gab uns eine angenehmere Syntax):

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

Typ-Kommentare waren zudem nützlich zur Unterstützung von Python 2, das keine integrierte Unterstützung für Typannotationen bietet:

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 tatsächlich keine besonders große Rolle spielten – die Vorteile der statischen Typisierung führten dazu, dass Benutzer schnell das nicht ganz perfekte Syntaxmuster vergaßen. Da im Python-Code mit kontrollierten Typen keine besonderen syntaktischen Konstruktionen mehr verwendet wurden, konnten die bestehenden Python-Tools und -Prozesse zur Codeverarbeitung weiterhin normal arbeiten, was den Entwicklern den Einstieg in das neue Werkzeug erheblich erleichterte.

Guido überzeugte mich außerdem, nach meiner Abschlussarbeit zu Dropbox zu kommen. Hier beginnt der spannendste Teil der Geschichte von mypy.

Die Fortsetzung folgt…

Sehr geehrte Leser! Wenn Sie Python nutzen, erzählen Sie uns bitte, welche Projekte Sie in dieser Sprache entwickeln.

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

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster