Python-Importautomatisierung

ZuNachher
import math import os.path import requests # 100500 other imports print(math.pi) print(os.path.join('my', 'path')) print(requests.get) 
 import smart_imports smart_imports.all() print(math.pi) print(os_path.join('my', 'path')) print(requests.get) 
So kam es, dass ich seit 2012 als einziger Programmierer einen Open-Source-Browser entwickle. In Python für sich. Der Browser ist nicht die einfachste Sache, jetzt gibt es im Hauptteil des Projekts mehr als 1000 Module und mehr als 120.000 Zeilen Python-Code. Insgesamt wird es bei Satellitenprojekten eineinhalb Mal so viel sein.

Irgendwann hatte ich es satt, am Anfang jeder Datei mit den Importböden herumzuspielen, und entschied mich, dieses Problem ein für alle Mal zu lösen. So wurde die smart_imports- Bibliothek geboren ( github , pypi ).

Die Idee ist ganz einfach. Jedes komplexe Projekt bildet schließlich eine eigene Vereinbarung über die Benennung von allem. Wenn diese Vereinbarung in formellere Regeln umgewandelt wird, kann jede Entität automatisch unter dem Namen der ihr zugeordneten Variablen importiert werden.

Zum Beispiel müssen Sie keine import math schreiben, import math auf math.pi - wir können math.pi verstehen, dass math in diesem Fall ein Modul der Standardbibliothek ist.

Intelligente Importe unterstützen Python> = 3.5. Die Bibliothek wird vollständig durch Tests abgedeckt, Abdeckung> 95% . Ich benutze es jetzt seit einem Jahr selbst.

Für Details lade ich Sie zu Cat ein.

Wie funktioniert es im Allgemeinen?


Der Code aus dem Header-Bild funktioniert also wie folgt:

  1. Während eines Aufrufs von smart_imports.all() Bibliothek den AST des Moduls, von dem aus der Aufruf erfolgt.
  2. Finden Sie nicht initialisierte Variablen;
  3. Wir führen den Namen jeder Variablen durch eine Folge von Regeln, die versuchen, das Modul (oder Modulattribut) zu finden, das für den Import nach Namen benötigt wird. Wenn eine Regel die erforderliche Entität gefunden hat, werden die folgenden Regeln nicht überprüft.
  4. Die gefundenen Module werden geladen, initialisiert und im globalen Namespace abgelegt (oder die erforderlichen Attribute dieser Module werden dort abgelegt).

Nicht initialisierte Variablen werden im gesamten Code durchsucht, einschließlich der neuen Syntax.

Der automatische Import ist nur für Projektkomponenten aktiviert, die explizit smart_imoprts.all() aufrufen. Darüber hinaus verbietet die Verwendung intelligenter Importe nicht die Verwendung herkömmlicher Importe. Auf diese Weise können Sie die Bibliothek schrittweise implementieren und komplexe zyklische Abhängigkeiten auflösen.

Ein akribischer Leser wird feststellen, dass das AST-Modul zweimal aufgebaut ist:

  • CPython erstellt es zum ersten Mal während des Modulimports.
  • Das zweite Mal, wenn smart_imports es während eines Aufrufs von smart_imports.all() .

AST kann wirklich nur einmal erstellt werden (dazu müssen Sie mithilfe von in PEP-0302 implementierten Import-Hooks in den Importprozess von Modulen integrieren, aber diese Lösung verlangsamt den Import.

Warum denkst du so?
Beim Vergleich der Leistung von zwei Implementierungen (mit und ohne Hooks) kam ich zu dem Schluss, dass CPython beim Importieren eines Moduls AST in seinen internen (C-shh) Datenstrukturen erstellt. Das Konvertieren in Python-Datenstrukturen ist teurer als das Erstellen eines Baums aus der Quelle mit dem ast- Modul.

Natürlich wird der AST jedes Moduls nur einmal pro Start erstellt und analysiert.

Standardimportregeln


Die Bibliothek kann ohne zusätzliche Konfiguration verwendet werden. Standardmäßig werden Module nach folgenden Regeln importiert:

  1. Durch genaues Zusammentreffen des Namens wird nach dem Modul neben dem aktuellen (im selben Verzeichnis) gesucht.
  2. Überprüft die Module der Standardbibliothek:
    • durch genaue Übereinstimmung des Namens für Pakete der obersten Ebene;
    • Bei verschachtelten Paketen und Modulen wird nach zusammengesetzten Namen gesucht und Punkte durch Unterstriche ersetzt. Beispielsweise wird os.path importiert, wenn die Variable os_path .
  3. Durch die genaue Übereinstimmung des Namens wird nach installierten Paketen von Drittanbietern gesucht. Zum Beispiel die bekannten Paketanfragen .

Leistung


Intelligente Importe wirken sich nicht auf die Leistung des Programms aus, erhöhen jedoch die Startzeit.

Aufgrund des Umbaus des AST erhöht sich die Zeit des ersten Laufs um das 1,5- bis 2-fache. Für kleine Projekte ist dies nicht von Bedeutung. In großen Projekten leidet die Startzeit eher unter der Abhängigkeitsstruktur zwischen den Modulen als unter der Importzeit eines bestimmten Moduls.

Wenn intelligente Importe populär werden, schreibe ich die Arbeit von AST auf C um - dies sollte die Startkosten erheblich senken.

Um das Laden zu beschleunigen, können die Ergebnisse der Verarbeitung von AST-Modulen im Dateisystem zwischengespeichert werden. Das Caching ist in der Konfiguration aktiviert. Natürlich ist der Cache deaktiviert, wenn Sie die Quelle ändern.

Die Startzeit wird sowohl von der Liste der Modul-Suchregeln als auch von deren Reihenfolge beeinflusst. Da einige Regeln die Standard-Python-Funktionalität verwenden, um nach Modulen zu suchen. Sie können diese Kosten ausschließen, indem Sie die Übereinstimmung von Namen und Modulen mithilfe der Regel "Benutzerdefinierte Namen" explizit angeben (siehe unten).

Konfiguration


Die Standardkonfiguration wurde bereits beschrieben. Es sollte ausreichen, in kleinen Projekten mit der Standardbibliothek zu arbeiten.

Standardkonfiguration
 { "cache_dir": null, "rules": [{"type": "rule_local_modules"}, {"type": "rule_stdlib"}, {"type": "rule_predefined_names"}, {"type": "rule_global_modules"}] } 


Bei Bedarf kann eine komplexere Konfiguration in das Dateisystem eingefügt werden.

Ein Beispiel für eine komplexe Konfiguration (über einen Browser).

Während eines Aufrufs von smart_import.all() ermittelt smart_import.all() Bibliothek die Position des aufrufenden Moduls im Dateisystem und sucht nach der Datei smart_imports.json in der Richtung vom aktuellen Verzeichnis zum Stammverzeichnis. Wenn eine solche Datei gefunden wird, wird sie als Konfiguration für das aktuelle Modul betrachtet.

Sie können mehrere verschiedene Konfigurationen verwenden (in verschiedenen Verzeichnissen ablegen).

Derzeit gibt es nicht viele Konfigurationsoptionen:

 { //     AST. //     null —   . "cache_dir": null|"string", //       . "rules": [] } 

Regeln importieren


Die Reihenfolge der Angabe von Regeln in der Konfiguration bestimmt die Reihenfolge ihrer Anwendung. Die erste Regel, die funktioniert hat, stoppt die weitere Suche nach Importen.

In den Beispielen für Konfigurationen wird die Regel rule_predefined_names häufig rule_predefined_names angezeigt. Es ist erforderlich, dass die integrierten Funktionen (z. B. print ) korrekt erkannt werden.

Regel 1: Vordefinierte Namen


Mit dieser Regel können Sie vordefinierte Namen wie __file__ und integrierte Funktionen wie print ignorieren.

Beispiel
 # : # { # "rules": [{"type": "rule_predefined_names"}] # } import smart_imports smart_imports.all() #        __file__ #        print(__file__) 

Regel 2: Lokale Module


Überprüft, ob sich neben dem aktuellen Modul (im selben Verzeichnis) ein Modul mit dem angegebenen Namen befindet. Wenn ja, importiert es.

Beispiel
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules"}] # } # #    : # # my_package # |-- __init__.py # |-- a.py # |-- b.py # b.py import smart_imports smart_imports.all() #    "a.py" print(a) 

Regel 3: Globale Module


Versucht, ein Modul direkt nach Namen zu importieren. Zum Beispiel das Anforderungsmodul .

Beispiel
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_global_modules"}] # } # #    # # pip install requests import smart_imports smart_imports.all() #    requests print(requests.get('http://example.com')) 

Regel 4: Benutzerdefinierte Namen


Entspricht dem Namen eines bestimmten Moduls oder seines Attributs. Die Konformität wird in der Regelkonfiguration angegeben.

Beispiel
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_custom", # "variables": {"my_import_module": {"module": "os.path"}, # "my_import_attribute": {"module": "random", "attribute": "seed"}}}] # } import smart_imports smart_imports.all() #       #        print(my_import_module) print(my_import_attribute) 

Regel 5: Standardmodule


Überprüft, ob der Name ein Standardbibliotheksmodul ist. Zum Beispiel math oder os.path, das sich in os_path verwandelt.

Es funktioniert schneller als die Regel zum Importieren globaler Module, da überprüft wird, ob ein Modul in einer zwischengespeicherten Liste vorhanden ist. Listen für jede Python-Version finden Sie hier: github.com/jackmaney/python-stdlib-list

Beispiel
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_stdlib"}] # } import smart_imports smart_imports.all() print(math.pi) 

Regel 6: Import nach Präfix


Importiert ein Modul nach Namen aus dem Paket, das seinem Präfix zugeordnet ist. Dies ist praktisch, wenn im gesamten Code mehrere Pakete verwendet werden. Beispielsweise kann auf die utils utils_ mit dem Präfix utils_ .

Beispiel
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_prefix", # "prefixes": [{"prefix": "utils_", "module": "my_package.utils"}]}] # } # #    : # # my_package # |-- __init__.py # |-- utils # |-- |-- __init__ # |-- |-- a.py # |-- |-- b.py # |-- subpackage # |-- |-- __init__ # |-- |-- c.py # c.py import smart_imports smart_imports.all() print(utils_a) print(utils_b) 

Regel 7: Das Modul aus dem übergeordneten Paket


Wenn Sie in verschiedenen Teilen des Projekts Unterpakete mit demselben Namen haben (z. B. tests oder migrations ), können Sie ihnen erlauben, nach Modulen zu suchen, die in den übergeordneten Paketen nach Namen importiert werden sollen.

Beispiel
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules_from_parent", # "suffixes": [".tests"]}] # } # #    : # # my_package # |-- __init__.py # |-- a.py # |-- tests # |-- |-- __init__ # |-- |-- b.py # b.py import smart_imports smart_imports.all() print(a) 

Regel 8: Bindung an ein anderes Paket


Für Module aus einem bestimmten Paket ermöglicht es die Suche nach Importen nach Namen in anderen Paketen (in der Konfiguration angegeben). In meinem Fall war diese Regel nützlich für Fälle, in denen ich die Arbeit der vorherigen Regel (Modul aus dem übergeordneten Paket) nicht auf das gesamte Projekt ausweiten wollte.

Beispiel
 # : # { # "rules": [{"type": "rule_predefined_names"}, # {"type": "rule_local_modules_from_namespace", # "map": {"my_package.subpackage_1": ["my_package.subpackage_2"]}}] # } # #    : # # my_package # |-- __init__.py # |-- subpackage_1 # |-- |-- __init__ # |-- |-- a.py # |-- subpackage_2 # |-- |-- __init__ # |-- |-- b.py # a.py import smart_imports smart_imports.all() print(b) 

Hinzufügen eigener Regeln


Das Hinzufügen einer eigenen Regel ist ziemlich einfach:

  1. Wir erben von der Klasse smart_imports.rules.BaseRule .
  2. Wir erkennen die notwendige Logik.
  3. Registrieren Sie eine Regel mit der Methode smart_imports.rules.register
  4. Fügen Sie die Regel zur Konfiguration hinzu.
  5. ???
  6. Gewinn

Ein Beispiel findet sich in der Umsetzung der aktuellen Regeln.

Gewinn


Mehrzeilige Importlisten am Anfang jeder Quelle sind verschwunden.

Die Anzahl der Zeilen hat abgenommen. Bevor der Browser auf intelligente Importe umstellte, waren 6688 Zeilen für den Import verantwortlich. Nach dem Übergang blieb 2084 übrig (zwei Zeilen smart_imports pro Datei + 130 Importe, die explizit von Funktionen und ähnlichen Stellen aufgerufen wurden).

Ein schöner Bonus war die Standardisierung der Namen im Projekt. Code ist leichter zu lesen und leichter zu schreiben. Sie müssen nicht über die Namen der importierten Entitäten nachdenken - es gibt einige klare Regeln, die leicht zu befolgen sind.

Entwicklungspläne


Ich mag die Idee, Codeeigenschaften durch Variablennamen zu definieren, daher werde ich versuchen, sie sowohl in intelligenten Importen als auch in anderen Projekten zu entwickeln.

In Bezug auf intelligente Importe plane ich:

  1. Unterstützung für neue Versionen von Python hinzufügen.
  2. Erkunden Sie die Möglichkeit, sich bei der Typanmerkung von Code auf die aktuellen Community-Praktiken zu verlassen.
  3. Entdecken Sie die Möglichkeit, faul zu importieren.
  4. Implementieren Sie Dienstprogramme zur automatischen Generierung einer Konfiguration aus Quellcodes und zum Refactoring von Quellen für die Verwendung von smart_imports.
  5. Schreiben Sie einen Teil des C-Codes neu, um die Arbeit mit dem AST zu beschleunigen.
  6. Entwicklung der Integration mit Lintern und IDEs, wenn diese Probleme mit der Codeanalyse ohne explizite Importe haben.

Darüber hinaus interessiert mich Ihre Meinung zum Standardverhalten der Bibliothek und zu den Importregeln.

Vielen Dank, dass Sie dieses Textblatt überwältigt haben :-D

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


All Articles