Gib alles ein

Hallo allerseits!


Wir haben bereits einen Artikel über die Entwicklung des Schreibens in Ostrovok.ru . Es erklärt, warum wir von pyContracts zu typeguard wechseln, warum wir zu typeguard wechseln und womit wir enden. Und heute werde ich Ihnen mehr darüber erzählen, wie dieser Übergang stattfindet.



Eine Funktionsdeklaration mit pyContracts sieht im Allgemeinen so aus:


from contracts import new_contract import datetime @new_contract def User (x): from models import User return isinstance(x, User) @new_contract def dt_datetime (x): return isinstance(x, datetime.datetime) @contract def func(user_list, amount, dt=None): """ :type user_list: list(User) :type amount: int|float :type dt: dt_datetime|None :rtype: bool """ 

Dies ist ein abstraktes Beispiel, da ich in unserem Projekt keine Definition einer Funktion gefunden habe, die in Bezug auf die Anzahl der Fälle für die Typprüfung kurz und aussagekräftig ist. In der Regel werden Definitionen für pyContracts in Dateien gespeichert, die keine andere Logik enthalten. Beachten Sie, dass hier Benutzer eine bestimmte Benutzerklasse ist und nicht direkt importiert wird.


Und das ist das gewünschte Ergebnis mit typeguard:


 from typechecked import typechecked from typing import List, Optional, Union from models import User import datetime @typechecked def func (user_list: List[User], amount: Union[int, float], dt: Optional[datetime.datetime]=None) -> bool: ... 

Im Allgemeinen gibt es im Projekt so viele Funktionen und Methoden mit Typprüfung, dass Sie den Mond erreichen können, wenn Sie sie in einem Stapel stapeln. Eine manuelle Übersetzung von pyContracts in typeguard ist also nicht möglich (ich habe es versucht!). Also habe ich beschlossen, ein Drehbuch zu schreiben.


Das Skript ist in zwei Teile unterteilt: Der eine Teil speichert die Importe neuer Verträge und der zweite Teil befasst sich mit dem Code-Refactoring.


Ich möchte darauf hinweisen, dass weder das eine noch das andere Skript den Anspruch erhebt, universell zu sein. Wir wollten kein Tool schreiben, um alle erforderlichen Fälle zu lösen. Daher habe ich häufig auf die automatische Bearbeitung einiger Sonderfälle verzichtet. Wenn sie im Projekt selten vorkommen, ist es schneller, sie von Hand zu beheben. Beispielsweise hat das Skript zum Generieren von Mapping-Verträgen und Importen 90% der Werte gesammelt, die restlichen 10% sind handgefertigte Mappings.


Die Logik des Skripts zum Generieren des Mappings:


Schritt 1. Durchsuchen Sie alle Dateien des Projekts und lesen Sie sie. Für jede Datei:


  • Wenn die Unterzeichenfolge "@new_contract" nicht vorhanden ist, überspringen Sie diese Datei.
  • Wenn dies der Fall ist, teilen Sie die Datei durch die Zeile "@new_contract". Für jeden Artikel:
    - Analyse für Definition und Import,
    - bei Erfolg in die Erfolgsdatei schreiben,
    - Wenn nicht, schreiben Sie in die Fehlerdatei.

Schritt 2. Fehler manuell verarbeiten


Nachdem wir nun die Namen aller von pyContracts verwendeten Typen (die mit dem Dekorator new_contract definiert wurden) und alle erforderlichen Importe haben, können wir Code für das Refactoring schreiben. Während ich manuell von pyContracts nach typeguard übersetzte, wurde mir klar, was ich aus dem Skript brauchte:


  1. Dies ist ein Befehl, der einen Modulnamen als Argument verwendet (es können mehrere verwendet werden), in dem die Syntax der Funktionsanmerkungen ersetzt werden muss.
  2. Sehen Sie sich alle Moduldateien an und lesen Sie sie. Für jede Datei:
    • Wenn keine Unterzeichenfolge "@contract" vorhanden ist, überspringen Sie diese Datei.
    • wenn ja, verwandle den Code in ast (abstrakter Syntaxbaum);
    • finden Sie alle Funktionen, die unter dem Vertrag Dekorator für jeden sind:
      • Dockstring abrufen, analysieren, dann löschen,
      • Erstellen Sie ein Wörterbuch der Form {arg_name: arg_type}. Verwenden Sie es, um die Funktionsanmerkung zu ersetzen.
      • Erinnern Sie sich an neue Importe,
    • schreibe den modifizierten Baum durch astunparse in eine Datei;
    • Fügen Sie neue Importe am Anfang der Datei hinzu.
    • Ersetzen Sie die Zeilen "@contract" durch "@typechecked", da dies einfacher ist als durch ast.

Lösen Sie die Frage "Ist dieser Name bereits in diese Datei importiert?" Das hatte ich von Anfang an nicht vor: Mit diesem Problem werden wir einen zusätzlichen Durchlauf der Isort-Bibliothek bewältigen.


Nachdem die erste Version des Skripts ausgeführt wurde, stellten sich jedoch Fragen, die noch gelöst werden mussten. Es stellte sich heraus, dass 1) ast nicht allmächtig ist, 2) astunparse allmächtiger ist, als wir möchten. Dies zeigte sich im Folgenden:


  • Beim Übergang zum Syntaxbaum verschwinden alle einzeiligen Kommentare aus dem Code.
  • leere Zeilen verschwinden ebenfalls;
  • ast unterscheidet nicht zwischen Funktionen und Methoden der Klasse, wir mussten Logik hinzufügen;
  • Umgekehrt werden beim Wechsel von einem Baum zu einem Code mehrzeilige Kommentare in dreifachen Anführungszeichen in einfache Anführungszeichen geschrieben und belegen eine Zeile. Neue Zeilenumbrüche werden durch \ n ersetzt.
  • Es erscheinen unnötige Klammern, wenn beispielsweise A und B und C oder D zu if ((A und B und C) oder D) werden.

Der Code, der durch ast und astunparse geleitet wird, funktioniert weiterhin, die Lesbarkeit ist jedoch eingeschränkt.


Der schwerwiegendste Nachteil ist das Verschwinden von einzeiligen Kommentaren (in anderen Fällen verlieren wir nichts, sondern nur Gewinn - zum Beispiel Klammern). Die Horast-Bibliothek, die auf ast, astunparse und tokenize basiert, verspricht, dies herauszufinden. Verspricht und tut.


Nun die leeren Zeilen. Es gab zwei mögliche Lösungen:


  1. tokenize weiß, wie man den "Sprachteil" einer Python bestimmt, und horast nutzt ihn aus, wenn er Kommentartoken erhält. Aber tokenize hat auch Token wie NewLine und NL. Sie müssen also sehen, wie Horast Kommentare wiederherstellt und kopiert, wobei der Tokentyp ersetzt wird.
    - schlug Anya vor, Erfahrung in der Entwicklung von 2 Monaten
  2. Da Horast Kommentare wiederherstellen kann, ersetzen wir zuerst alle leeren Zeilen durch einen bestimmten Kommentar, überspringen dann Horast und ersetzen unseren Kommentar durch eine leere Zeile.
    - kam mit Eugene, Erfahrung in der Entwicklung von 8 Jahren

In den Kommentaren werde ich auf die dreifachen Anführungszeichen eingehen. Es war recht einfach, zusätzliche Klammern einzufügen, zumal einige von ihnen durch automatische Formatierung entfernt werden.


In horast verwenden wir zwei Funktionen: parse und unparse, aber beide sind nicht ideal - parse enthält seltsame interne Fehler, in seltenen Fällen kann es den Quellcode nicht analysieren und unparse kann nichts schreiben, das type hat (so einen Typ, der Es stellt sich heraus, ob Sie tippen (any_other_type)).


Ich habe mich gegen das Parsen entschieden, weil die Logik der Arbeit ziemlich verwirrend ist und Ausnahmen selten sind - das Prinzip der Nichtuniversalität funktioniert hier.


Aber unparese wirkt sehr übersichtlich und recht elegant. Die Funktion unparse erstellt eine Instanz der Unparser-Klasse, die in init den Baum verarbeitet und dann in eine Datei schreibt. Horast.Unparser wird sukzessive von vielen anderen Unparsern geerbt, wobei die Basisklasse astunparse.Unparser ist. Alle abgeleiteten Klassen erweitern einfach die Funktionalität der Basisklasse, aber die Logik der Arbeit bleibt gleich. Betrachten Sie also astunparse.Unparser. Es gibt fünf wichtige Methoden:


  1. Schreiben - schreibt einfach etwas in eine Datei.
  2. fill - verwendet Write basierend auf der Anzahl der Einrückungen (die Anzahl der Einrückungen wird als Klassenfeld gespeichert).
  3. enter - erhöht den Einzug.
  4. leave - verkleinert den Einzug.
  5. dispatch - bestimmt den Typ des Knotens des Baums (sagen wir T), ruft die entsprechende Methode mit dem Namen des Knotentyps auf, jedoch mit einem Unterstrich (d. h. _T). Dies ist eine Metamethode.

Alle anderen Methoden sind Methoden der Form _T, z. B. _Module oder _Str. In jeder dieser Methoden kann sie: 1) rekursiv für Teilbaumknoten versenden oder 2) den Inhalt des Knotens mit write schreiben oder Zeichen und Schlüsselwörter hinzufügen, sodass das Ergebnis ein gültiger Ausdruck in Python ist.


Wir sind zum Beispiel auf einen Knoten vom Typ arg gestoßen, in dem ast den Argumentnamen und den Annotationsknoten speichert. Dann ruft dispatch die _arg-Methode auf, die zuerst den Namen des Arguments schreibt, dann den Doppelpunkt schreibt und dispatch für den Annotationsknoten ausführt, wobei der Annotationsteilbaum analysiert wird und dispatch und write weiterhin für diesen Teilbaum aufgerufen werden.


Kehren wir zu unserem Problem der Unmöglichkeit der Art der Verarbeitung zurück. Nachdem Sie nun verstanden haben, wie unspezifisch funktioniert, ist das Erstellen Ihres Typs ganz einfach. Lassen Sie uns einen Typ erstellen:


 class NewType(object): def __init__ (self, t): self.s = ts 

Es speichert einen String in sich und nicht nur so: Wir müssen Funktionsargumente typisieren und wir erhalten die Argumenttypen in Form von Strings aus dem Andocken. Ersetzen Sie Argumentanmerkungen daher nicht durch die von uns benötigten Typen, sondern durch ein NewType-Objekt, in dem nur der Name des gewünschten Typs gespeichert ist.


Dazu erweitern Sie horast.Unparser - schreiben Sie Ihr UnparserWithType, das von horast.Unparser erbt, und fügen Sie die Verarbeitung unseres neuen Typs hinzu.


 class UnparserWithType(horast.Unparser): def _NewType (self, t): self.write(ts) 

Dies verbindet sich mit dem Geist der Bibliothek. Die Namen der Variablen sind im Stil von ast erstellt, und deshalb bestehen sie aus einem Buchstaben, und nicht, weil ich mir keine Namen vorstellen kann. Ich denke, dass t für tree und s für string steht. NewType ist übrigens keine Zeichenfolge. Wenn wir wollten, dass es als Zeichenfolgentyp interpretiert wird, müssten wir vor und nach dem Schreibaufruf Anführungszeichen schreiben.


Und jetzt die Magie affen patch: ersetze horast.Unparser durch unseren UnparserWithType.


So funktioniert es jetzt: Wir haben einen Syntaxbaum, er hat eine Funktion, Funktionen haben Argumente, Argumente haben Typanmerkungen, eine Nadel ist in der Typanmerkung versteckt und Koshcheevs Tod ist darin versteckt. Bisher gab es überhaupt keine Anmerkungsknoten, wir haben sie erstellt, und jeder dieser Knoten ist eine Instanz von NewType. Wir nennen die Funktion unparse für unseren Baum und für jeden Knoten, den er dispatch nennt, die diesen Knoten klassifiziert und seine entsprechende Funktion aufruft. Sobald die dispatch-Funktion den Node des Arguments empfängt, schreibt sie den Namen des Arguments und prüft, ob eine Annotation vorhanden ist (früher war es None, aber wir setzen NewType dort ein). Wenn dies der Fall ist, schreibt sie einen Doppelpunkt und ruft dispatch für die Annotation auf, die unser _NewType aufruft, welches Schreibt einfach die Zeichenfolge, die darin gespeichert ist - dies ist der Typname. Als Ergebnis erhalten wir das schriftliche Argument: type.


Eigentlich ist das nicht ganz legal. Aus der Sicht des Compilers haben wir die Annotationen der Argumente mit einigen Wörtern niedergeschrieben, die nirgendwo definiert sind. Wenn unsparse seine Arbeit beendet, erhalten wir den falschen Code: Wir brauchen Importe. Ich bilde einfach eine Zeile des richtigen Formats und füge sie am Anfang der Datei ein. Anschließend hänge ich das Ergebnis an "unparse" an, obwohl ich dem Syntaxbaum Importe als Knoten hinzufügen könnte, da ast Import- und ImportFrom-Knoten unterstützt.


Das Problem der dreifachen Anführungszeichen zu lösen ist nicht schwieriger als das Hinzufügen eines neuen Typs. Wir werden die StrType-Klasse und die _StrType-Methode erstellen. Die Methode unterscheidet sich nicht von der _NewType-Methode, die zum Kommentieren von Typen verwendet wird, aber die Definition der Klasse hat sich geändert: Wir speichern nicht nur die Zeichenfolge selbst, sondern auch die Registerebene, auf der sie geschrieben werden soll. Die Anzahl der Einrückungen ist wie folgt definiert: Wenn diese Zeile in einer Funktion vorkommt, dann eine, wenn in einer Methode, dann zwei, und es gibt keine Fälle, in denen die Funktion im Hauptteil einer anderen Funktion definiert ist und gleichzeitig in unserem Projekt dekoriert wird.


 class StrType(object): def __init__ (self, s, indent): self.s = s self.indent = indent def __repr__ (self): return '"""\n' + self.s + '\n' + ' ' * 4 * self.indent + '"""\n' 

In repr definieren wir, wie unsere Linie aussehen soll. Ich denke, das ist bei weitem nicht die einzige Lösung, aber es funktioniert. Man könnte mit astunparse.fill und astunparse.Unparser.indent experimentieren, dann wäre es universeller, aber diese Idee kam mir schon beim Schreiben dieses Artikels in den Sinn.


Damit enden gelöste Schwierigkeiten. Nach dem Ausführen meines Skripts tritt manchmal das Problem des zyklischen Imports auf, dies ist jedoch eine Frage der Architektur. Ich habe keine fertige Lösung von Drittanbietern gefunden, und es scheint eine ernsthafte Komplikation der Aufgabe zu sein, solche Fälle im Rahmen meines Skripts zu behandeln. Mit Hilfe von ast ist es wahrscheinlich möglich, zyklische Importe zu erkennen und aufzulösen, aber diese Idee muss separat betrachtet werden. Im Allgemeinen erlaubte mir die vernachlässigbare Anzahl solcher Vorfälle in unserem Projekt, sie nicht automatisch zu verarbeiten.


Eine andere Schwierigkeit, auf die ich gestoßen bin, war der Mangel an Ausdrucksverarbeitung in Astroimport, da ein vorsichtiger Leser bereits weiß, dass das Affenpflaster das Heilmittel für alle Krankheiten ist. Lassen Sie dies seine Hausaufgabe für ihn sein, aber ich habe mich dazu entschlossen: Fügen Sie einfach solche Importe zur Zuordnungsdatei hinzu, da diese Konstruktion normalerweise verwendet wird, um den Namenskonflikt zu umgehen, und wir haben nur wenige davon.


Trotz der festgestellten Unvollkommenheiten macht das Skript das, was es beabsichtigt hatte. Was ist das Ergebnis:


  1. Die Zeit, für die das Projekt gestartet wird, wurde von 10 auf 3 Sekunden verringert.
  2. Die Anzahl der Dateien hat sich aufgrund des Entfernens der new_contract-Definitionen verringert. Die Dateien selbst wurden verkleinert: Ich habe nicht gemessen, aber im Durchschnitt summierte sich das Git auf n hinzugefügte und 2n gelöschte Zeilen.
  3. Smart IDEs begannen, andere Hinweise zu geben, denn jetzt sind es keine Kommentare, sondern ehrliche Importe;
  4. Die Lesbarkeit wurde verbessert.
  5. Irgendwo tauchten Klammern auf.

Vielen Dank!


Nützliche Links:


  1. Ast
  2. Horast
  3. Alle Arten von Astknoten und was in ihnen gespeichert ist
  4. Zeigt schön den Syntaxbaum
  5. Isort

Source: https://habr.com/ru/post/de480930/


All Articles