PEP 402 – Vereinfachte Paketstruktur und Partitionierung
- Autor:
- Phillip J. Eby
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Thema:
- Packaging
- Erstellt:
- 12-Jul-2011
- Python-Version:
- 3.3
- Post-History:
- 20-Jul-2011
- Ersetzt:
- 382
Ablehnungsbescheid
Am ersten Tag der Sprints auf der US PyCon 2012 hatten wir eine lange und fruchtbare Diskussion über PEP 382 und PEP 402. Wir haben beide abgelehnt, aber eine neue PEP wird geschrieben werden, um im Geiste von PEP 402 fortzufahren. Martin von Löwis hat eine Zusammenfassung verfasst: [3].
Zusammenfassung
Diese PEP schlägt eine Erweiterung des Python-Paket-Importmechanismus vor, um
- Benutzer anderer Sprachen weniger zu überraschen,
- es einfacher zu machen, ein Modul in ein Paket umzuwandeln, und
- die Aufteilung von Paketen in separat installierbare Komponenten zu unterstützen (ähnlich wie „Namespace-Pakete“, wie in PEP 382 beschrieben).
Die vorgeschlagenen Erweiterungen ändern die Semantik keiner derzeit importierbaren Verzeichnisstruktur, sondern ermöglichen es Paketen, eine vereinfachte Verzeichnisstruktur zu verwenden (die derzeit nicht importierbar ist).
Die vorgeschlagenen Änderungen fügen jedoch KEINE zusätzlichen Leistungs-Overheads für den Import bestehender Module oder Pakete hinzu, und die Leistung für die neue Verzeichnisstruktur sollte ungefähr gleich sein wie die früherer „Namespace-Paket“-Lösungen (wie z. B. pkgutil.extend_path()).
Das Problem
„Die meisten Pakete sind wie Module. Ihre Inhalte sind stark voneinander abhängig und können nicht auseinandergenommen werden. [Jedoch] existieren einige Pakete, um einen separaten Namensraum bereitzustellen. … Es sollte möglich sein, Unterpakete oder Untermodule dieser [Namespace-Pakete] unabhängig voneinander zu verteilen.“— Jim Fulton, kurz vor der Veröffentlichung von Python 2.3 [1]
Wenn neue Benutzer aus anderen Sprachen zu Python kommen, sind sie oft verwirrt über die Paketimport-Semantik von Python. Bei Google zum Beispiel erhielt Guido Beschwerden von einer „großen Menge mit Mistgabeln“ [2], dass die Anforderung, dass Pakete ein __init__-Modul enthalten müssen, ein „Fehlmerkmal“ sei und abgeschafft werden sollte.
Darüber hinaus sind Benutzer, die aus Sprachen wie Java oder Perl kommen, manchmal verwirrt über einen Unterschied bei der Suche im Python-Importpfad.
In den meisten anderen Sprachen mit einem ähnlichen Pfadmechanismus wie sys.path von Python ist ein Paket lediglich ein Namensraum, der Module oder Klassen enthält, und kann daher über mehrere Verzeichnisse im Pfad der Sprache verteilt sein. In Perl beispielsweise wird nach einem Foo::Bar-Modul in Foo/-Unterverzeichnissen entlang des Modul-Include-Pfads gesucht, nicht nur im ersten gefundenen Unterverzeichnis.
Schlimmer noch, dies ist nicht nur ein Problem für neue Benutzer: Es hindert jeden daran, ein Paket einfach in separat installierbare Komponenten aufzuteilen. In Perl-Begriffen wäre es so, als müssten jedes mögliche Net::-Modul auf CPAN in einem einzigen Tarball gebündelt und versendet werden!
Aus diesem Grund existieren verschiedene Workarounds für diese letztere Einschränkung, die unter dem Begriff „Namespace-Pakete“ zirkulieren. Die Python-Standardbibliothek bietet seit Python 2.3 einen solchen Workaround (über die Funktion pkgutil.extend_path()), und das Paket „setuptools“ bietet einen weiteren (über pkg_resources.declare_namespace()).
Die Workarounds selbst fallen jedoch einem *dritten* Problem mit Pythons Art, Pakete im Dateisystem zu organisieren, zum Opfer.
Da ein Paket ein __init__-Modul enthalten *muss*, muss jeder Versuch, Module für dieses Paket zu verteilen, notwendigerweise dieses __init__-Modul enthalten, wenn diese Module importierbar sein sollen.
Die Tatsache, dass jede Verteilung von Modulen für ein Paket dieses (duplizierte) __init__-Modul enthalten muss, bedeutet jedoch, dass OS-Anbieter, die diese Modulverteilungen paketieren, den Konflikt bewältigen müssen, der durch die Installation desselben __init__-Moduls an derselben Stelle im Dateisystem durch mehrere Modulverteilungen entsteht.
Dies führte zur Einreichung von PEP 382 („Namespace Packages“) – einer Möglichkeit, die Importmechanismen von Python so zu signalisieren, dass ein Verzeichnis importierbar ist, unter Verwendung eindeutiger Dateinamen pro Modulverteilung.
Es gab jedoch mehr als einen Nachteil bei diesem Ansatz. Die Leistung aller Importvorgänge wäre betroffen, und der Prozess der Kennzeichnung eines Pakets wurde noch komplexer. Neue Terminologie musste erfunden werden, um die Lösung zu erklären, und so weiter.
Während die Diskussion über die Terminologie auf der Import-SIG fortgesetzt wurde, wurde bald klar, dass der Hauptgrund, warum die Konzepte im Zusammenhang mit „Namespace-Paketen“ so schwer zu erklären waren, darin lag, dass Pythons derzeitige Art, Pakete zu handhaben, im Vergleich zu anderen Sprachen eher unterentwickelt ist.
Das heißt, in anderen gängigen Sprachen mit Paketsystemen wird kein spezieller Begriff benötigt, um „Namespace-Pakete“ zu beschreiben, da *alle* Pakete im Allgemeinen das gewünschte Verhalten zeigen.
Anstatt ein isoliertes einzelnes Verzeichnis mit einem speziellen Markermodul zu sein (wie in Python), sind Pakete in anderen Sprachen typischerweise einfach die *Vereinigung* von Verzeichnissen mit passenden Namen über den *gesamten* Import- oder Inklusionspfad.
In Perl beispielsweise wird das Modul Foo immer in einer Foo.pm-Datei gefunden, und ein Modul Foo::Bar wird immer in einer Foo/Bar.pm-Datei gefunden. (Mit anderen Worten, es gibt einen offensichtlichen Weg, den Speicherort eines bestimmten Moduls zu finden.)
Dies liegt daran, dass Perl ein Modul als *unterschiedlich* von einem Paket betrachtet: Das Paket ist rein ein *Namensraum*, in dem andere Module residieren können, und ist nur *zufällig* auch der Name eines Moduls.
In aktuellen Python-Versionen sind jedoch das Modul und das Paket enger miteinander verbunden. Foo ist immer ein Modul – egal ob es in Foo.py oder Foo/__init__.py gefunden wird – und es ist eng mit seinen Untermodulen (falls vorhanden) verbunden, die sich im exakt selben Verzeichnis befinden müssen, in dem __init__.py gefunden wurde.
Positiv daran ist, dass diese Designentscheidung bedeutet, dass ein Paket sehr eigenständig ist und als Einheit installiert, kopiert usw. werden kann, indem einfach eine Operation auf dem Stammverzeichnis des Pakets durchgeführt wird.
Negativ ist jedoch, dass es für Anfänger unintuitiv ist und einen komplexeren Schritt erfordert, um ein Modul in ein Paket umzuwandeln. Wenn Foo als Foo.py beginnt, muss es verschoben und in Foo/__init__.py umbenannt werden.
Umgekehrt, wenn Sie von Anfang an ein Foo.Bar-Modul erstellen möchten, aber keine bestimmten Modulinhalte für Foo selbst haben, dann müssen Sie eine leere und scheinbar irrelevante Foo/__init__.py-Datei erstellen, damit Foo.Bar importierbar ist.
(Und diese Probleme verwirren nicht nur Neulinge in der Sprache: Sie ärgern auch viele erfahrene Entwickler.)
Daher wurde nach einigen Diskussionen auf der Import-SIG diese PEP als Alternative zu PEP 382 erstellt, in dem Versuch, *alle* oben genannten Probleme zu lösen, nicht nur die „Namespace-Paket“-Anwendungsfälle.
Und als erfreulicher Nebeneffekt beeinträchtigt die in dieser PEP vorgeschlagene Lösung nicht die Importleistung normaler Module oder eigenständiger (d. h. __init__-basierter) Pakete.
Die Lösung
In der Vergangenheit wurden verschiedene Vorschläge gemacht, um intuitivere Ansätze zur Paketverzeichnisstruktur zu ermöglichen. Die meisten scheiterten jedoch an einem offensichtlichen Abwärtskompatibilitätsproblem.
Das heißt, wenn die Anforderung für ein __init__-Modul einfach fallen gelassen würde, würde dies die Möglichkeit eröffnen, dass ein Verzeichnis namens, sagen wir, string auf sys.path den Import des string-Moduls der Standardbibliothek blockieren könnte.
Paradoxerweise ergibt sich das Scheitern dieses Ansatzes jedoch *nicht* aus der Abschaffung der __init__-Anforderung!
Vielmehr ergibt sich das Scheitern daraus, dass der zugrundeliegende Ansatz als selbstverständlich annimmt, dass ein Paket nur EINE Sache ist, anstatt zwei.
In Wahrheit besteht ein Paket aus zwei separaten, aber zusammenhängenden Einheiten: einem Modul (mit eigenen, optionalen Inhalten) und einem *Namensraum*, in dem *andere* Module oder Pakete gefunden werden können.
In aktuellen Python-Versionen werden jedoch der Modulteil (gefunden in __init__) und der Namensraum für Untermodulimporte (repräsentiert durch das __path__-Attribut) beide gleichzeitig initialisiert, wenn das Paket zum ersten Mal importiert wird.
Und wenn man davon ausgeht, dass dies der *einzige* Weg ist, diese beiden Dinge zu initialisieren, dann gibt es keine Möglichkeit, die Notwendigkeit eines __init__-Moduls fallen zu lassen und gleichzeitig abwärtskompatibel mit bestehenden Verzeichnisstrukturen zu bleiben.
Schließlich, sobald Sie ein Verzeichnis auf sys.path finden, das dem gewünschten Namen entspricht, bedeutet das, dass Sie das Paket „gefunden“ haben und die Suche stoppen müssen, richtig?
Nun, nicht ganz.
Ein Gedankenexperiment
Springen wir für einen Moment in die Zeitmaschine und tun so, als wären wir in den frühen 1990er Jahren, kurz bevor Python-Pakete und __init__.py erfunden wurden. Aber stellen Sie sich vor, wir sind bereits mit Perl-ähnlichen Paketimporten vertraut und möchten ein ähnliches System in Python implementieren.
Wir hätten immer noch die *Modul*-Importe von Python als Grundlage, daher könnten wir uns sicherlich Foo.py als übergeordnetes Foo-Modul für ein Foo-Paket vorstellen. Aber wie würden wir Untermodul- und Unterpaketimporte implementieren?
Nun, wenn wir die Idee von __path__-Attributen noch nicht hätten, würden wir wahrscheinlich sys.path durchsuchen, um nach Foo/Bar.py zu suchen.
Aber wir würden es *nur* tun, wenn jemand tatsächlich versuchen würde, Foo.Bar zu importieren.
NICHT, wenn sie Foo importieren.
Und *das* ermöglicht es uns, das Abwärtskompatibilitätsproblem des Wegfalls der __init__-Anforderung hier im Jahr 2011 zu beseitigen.
Wie?
Nun, wenn wir import Foo, dann *suchen* wir nicht einmal nach Foo/-Verzeichnissen auf sys.path, weil es uns *noch* nicht interessiert. Der einzige Zeitpunkt, an dem es uns interessiert, ist der Zeitpunkt, an dem jemand tatsächlich versucht, ein Untermodul oder Unterpaket von Foo zu importieren.
Das bedeutet, wenn Foo beispielsweise ein Modul der Standardbibliothek ist und ich ein Foo-Verzeichnis auf sys.path habe (natürlich ohne __init__.py), dann bricht *nichts*. Das Foo-Modul ist immer noch nur ein Modul und wird weiterhin normal importiert.
Eigenständige vs. „virtuelle“ Pakete
Natürlich schlägt der Versuch, import Foo.Bar auszuführen, im heutigen Python fehl, wenn Foo nur ein Foo.py-Modul ist (und somit kein __path__-Attribut hat).
Daher schlägt diese PEP vor, ein __path__ zu *dynamisch* zu erstellen, falls eines fehlt.
Das heißt, wenn ich versuche, import Foo.Bar auszuführen, wird die vorgeschlagene Änderung an der Import-Maschinerie feststellen, dass dem Foo-Modul ein __path__ fehlt, und daher versuchen, eines zu *erstellen*, bevor es fortfährt.
Und das wird geschehen, indem eine Liste aller vorhandenen Foo/-Verzeichnisse in den auf sys.path gelisteten Verzeichnissen zusammengestellt wird.
Wenn die Liste leer ist, schlägt der Import mit ImportError fehl, genau wie heute. Wenn die Liste jedoch *nicht* leer ist, wird sie in einem neuen Foo.__path__-Attribut gespeichert, wodurch das Modul zu einem „virtuellen Paket“ wird.
Das heißt, da es nun ein gültiges __path__ hat, können wir mit dem normalen Import von Untermodulen oder Unterpaketen fortfahren.
Beachten Sie nun, dass diese Änderung „klassische“, eigenständige Pakete mit einem __init__-Modul nicht beeinträchtigt. Solche Pakete haben bereits ein __path__-Attribut (initialisiert zur Importzeit), sodass die Import-Maschinerie später kein weiteres erstellen wird.
Dies bedeutet, dass (zum Beispiel) das Standardbibliotheks-Paket email in keiner Weise davon betroffen ist, dass Sie eine Reihe von nicht zusammenhängenden Verzeichnissen namens email auf sys.path haben. (Selbst wenn sie *.py-Dateien enthalten.)
Aber es bedeutet, dass, wenn Sie Ihr Foo-Modul in ein Foo-Paket umwandeln möchten, Sie lediglich ein Foo/-Verzeichnis irgendwo auf sys.path hinzufügen und beginnen müssen, Module darin abzulegen.
Aber was, wenn Sie nur ein „Namespace-Paket“ wollen? Das heißt, ein Paket, das *nur* ein Namensraum für verschiedene separat verteilte Untermodule und Unterpakete ist?
Wenn Sie beispielsweise Zope Corporation sind und Dutzende von separaten Tools wie zc.buildout vertreiben, jeweils in Paketen unter dem zc-Namensraum, möchten Sie nicht jedes von Ihnen ausgelieferte Tool mit einem leeren zc.py erstellen und einfügen. (Und wenn Sie ein Linux- oder anderer OS-Anbieter sind, möchten Sie sich nicht mit den Paketinstallationskonflikten auseinandersetzen, die durch den Versuch entstehen, zehn Kopien von zc.py am selben Ort zu installieren!)
Kein Problem. Wir müssen nur noch eine kleine Änderung am Importprozess vornehmen: Wenn der „klassische“ Importprozess kein eigenständiges Modul oder Paket findet (z. B. wenn import zc kein zc.py oder zc/__init__.py findet), dann versuchen wir erneut, einen __path__ zu erstellen, indem wir nach allen zc/-Verzeichnissen auf sys.path suchen und diese in eine Liste aufnehmen.
Wenn diese Liste leer ist, lösen wir ImportError aus. Wenn sie jedoch nicht leer ist, erstellen wir ein leeres zc-Modul und legen die Liste in zc.__path__. Glückwunsch: zc ist nun ein reines virtuelles Paket, das nur als Namensraum dient! Es hat keine Modulinhalte, aber Sie können trotzdem Untermodule und Unterpakete daraus importieren, unabhängig davon, wo sie sich auf sys.path befinden.
(Übrigens gelten beide diese Ergänzungen zum Importprotokoll (d. h. der dynamisch hinzugefügte __path__ und dynamisch erstellte Module) rekursiv für Kindpakete, wobei der __path__ des Elternpakets anstelle von sys.path als Grundlage für die Generierung eines Kind- __path__ dient. Das bedeutet, dass eigenständige und virtuelle Pakete einander unbegrenzt enthalten können, mit der Einschränkung, dass, wenn Sie ein virtuelles Paket in ein eigenständiges einfügen, es einen sehr kurzen __path__ haben wird!)
Abwärtskompatibilität und Leistung
Beachten Sie, dass diese beiden Änderungen *nur* Importvorgänge betreffen, die heute zu ImportError führen würden. Daher ist die Leistung von Importen, die keine virtuellen Pakete betreffen, unbeeinflusst, und potenzielle Abwärtskompatibilitätsprobleme sind sehr begrenzt.
Heute ist es ein sofortiger Fehler, wenn Sie versuchen, Untermodule oder Unterpakete aus einem Modul ohne __path__ zu importieren. Und natürlich würde import zc heute ebenfalls fehlschlagen, wenn kein zc.py oder zc/__init__.py irgendwo auf sys.path vorhanden ist.
Somit sind die einzigen potenziellen Abwärtskompatibilitätsprobleme
- Werkzeuge, die erwarten, dass Paketverzeichnisse ein
__init__-Modul haben, die erwarten, dass Verzeichnisse ohne ein__init__-Modul nicht importierbar sind, oder die erwarten, dass__path__-Attribute statisch sind, werden virtuelle Pakete nicht als Pakete erkennen.(In der Praxis bedeutet dies nur, dass Werkzeuge aktualisiert werden müssen, um virtuelle Pakete zu unterstützen, z. B. durch Verwendung von
pkgutil.walk_modules()anstelle von hartkodierten Dateisystem-Suchen.) - Code, der erwartet, dass bestimmte Importe fehlschlagen, könnte nun etwas Unerwartetes tun. Dies sollte in der Praxis ziemlich selten sein, da die meisten seriösen, nicht-Test-Codes Dinge nicht importieren, von denen erwartet wird, dass sie nicht existieren!
Die größte wahrscheinliche Ausnahme wäre, wenn ein Code-Teil versucht zu prüfen, ob ein Paket installiert ist, indem er es importiert. Wenn dies *nur* durch Importieren eines Top-Level-Moduls geschieht (d. h. nicht durch Überprüfung von __version__ oder einem anderen Attribut), *und* ein Verzeichnis mit demselben Namen wie das gesuchte Paket irgendwo auf sys.path vorhanden ist, *und* das Paket tatsächlich nicht installiert ist, dann könnte ein solcher Code getäuscht werden und denken, ein Paket sei installiert, das es tatsächlich nicht ist.
Angenommen, jemand schreibt beispielsweise ein Skript (datagen.py) mit dem folgenden Code
try:
import json
except ImportError:
import simplejson as json
Und führt es in einem Verzeichnis aus, das wie folgt aufgebaut ist
datagen.py
json/
foo.js
bar.js
Wenn import json aufgrund der bloßen Anwesenheit des json/-Unterverzeichnisses erfolgreich wäre, würde der Code fälschlicherweise glauben, dass das json-Modul verfügbar ist, und mit einem Fehler fortfahren.
Wir können jedoch verhindern, dass Eckfälle wie diese auftreten, indem wir einfach eine kleine Änderung am bisher vorgestellten Algorithmus vornehmen. Anstatt zu erlauben, ein „rein virtuelles“ Paket (wie zc) zu importieren, erlauben wir nur den Import des *Inhalts* virtueller Pakete.
Das heißt, eine Anweisung wie import zc sollte ImportError auslösen, wenn kein zc.py oder zc/__init__.py auf sys.path vorhanden ist. Aber import zc.buildout sollte trotzdem erfolgreich sein, solange es ein zc/buildout.py oder zc/buildout/__init__.py auf sys.path gibt.
Mit anderen Worten, wir erlauben keine direkten Importe von reinen virtuellen Paketen, sondern nur von Modulen und eigenständigen Paketen. (Dies ist eine akzeptable Einschränkung, da das alleinige Importieren eines solchen Pakets keinen *funktionalen* Wert hat. Schließlich wird das Modulobjekt keine *Inhalte* haben, bis Sie mindestens eines seiner Unterpakete oder Untermodule importieren!)
Sobald zc.buildout erfolgreich importiert wurde, wird es ein zc-Modul in sys.modules geben, und der Versuch, es zu importieren, wird natürlich erfolgreich sein. Wir verhindern nur einen *anfänglichen* Import, um falsch positive Import-Erfolge bei kollidierenden Unterverzeichnissen auf sys.path zu verhindern.
Mit dieser kleinen Änderung funktioniert das datagen.py-Beispiel oben korrekt. Wenn es import json ausführt, hat die bloße Anwesenheit eines json/-Verzeichnisses keinerlei Einfluss auf den Importprozess, selbst wenn es .py-Dateien enthält. Das json/-Verzeichnis wird nur dann durchsucht, wenn ein Import wie import json.converter versucht wird.
Gleichzeitig können Werkzeuge, die Pakete und Module durch Durchlaufen einer Verzeichnisstruktur finden müssen, aktualisiert werden, um die vorhandene pkgutil.walk_modules()-API zu verwenden, und Werkzeuge, die Pakete im Speicher inspizieren müssen, sollten die anderen in der Sektion Standard Library Changes/Additions unten beschriebenen APIs verwenden.
Spezifikation
Es wird eine Änderung am bestehenden Importprozess vorgenommen, wenn Namen importiert werden, die mindestens einen . enthalten – d. h. Importe von Modulen, die ein übergeordnetes Paket haben.
Insbesondere, wenn das übergeordnete Paket nicht existiert, oder existiert, aber kein __path__-Attribut hat, wird zunächst versucht, einen „virtuellen Pfad“ für das übergeordnete Paket zu erstellen (gemäß dem Algorithmus, der im Abschnitt über virtuelle Pfade unten beschrieben ist).
Wenn der berechnete „virtuelle Pfad“ leer ist, führt dies zu einem ImportError, genau wie heute. Wenn jedoch ein nicht leerer virtueller Pfad erhalten wird, wird der normale Import des Untermoduls oder Unterpakets fortgesetzt, wobei dieser virtuelle Pfad zum Auffinden des Untermoduls oder Unterpakets verwendet wird. (Genau wie es mit dem __path__ des Elternteils der Fall wäre, wenn das Elternpaket existiert und ein __path__ hätte.)
Wenn ein Untermodul oder Unterpaket gefunden (aber noch nicht geladen) wird, wird das Elternpaket erstellt und zu sys.modules hinzugefügt (falls es zuvor nicht existierte) und sein __path__ wird auf den berechneten virtuellen Pfad gesetzt (falls er noch nicht gesetzt war).
Auf diese Weise, wenn die eigentliche Lade des Untermoduls oder Unterpakets stattfindet, wird ein existierendes Elternpaket erkannt und alle relativen Importe funktionieren korrekt. Wenn jedoch kein Untermodul oder Unterpaket existiert, wird das Elternpaket *nicht* erstellt, noch wird ein eigenständiges Modul in ein Paket umgewandelt (durch Hinzufügen eines fiktiven __path__-Attributs).
Beachten Sie übrigens, dass diese Änderung *rekursiv* angewendet werden muss: Wenn foo und foo.bar reine virtuelle Pakete sind, dann muss import foo.bar.baz warten, bis foo.bar.baz gefunden wurde, bevor Modulobjekte für *sowohl* foo als auch foo.bar erstellt werden, und dann beide zusammen erstellt werden, wobei das foo-Modul sein .bar-Attribut ordnungsgemäß auf das foo.bar-Modul gesetzt wird.
Auf diese Weise sind reine virtuelle Pakete niemals direkt importierbar: Ein alleiniger import foo oder import foo.bar schlägt fehl, und die entsprechenden Module erscheinen nicht in sys.modules, bis sie benötigt werden, um auf ein *erfolgreich* importiertes Untermodul oder eigenständiges Unterpaket zu verweisen.
Virtuelle Pfade
Ein virtueller Pfad wird erstellt, indem ein PEP 302 „Importer“-Objekt für jeden der in sys.path gefundenen Pfadeinträge (für ein Top-Level-Modul) oder den übergeordneten __path__ (für ein Untermodul) erhalten wird.
(Hinweis: Da sys.meta_path-Importer nicht mit Pfadeintragsstrings von sys.path oder __path__ verbunden sind, nehmen solche Importer *nicht* an diesem Prozess teil.)
Jeder Importer wird auf eine get_subpath()-Methode überprüft, und falls vorhanden, wird die Methode mit dem vollständigen Namen des Moduls/Pakets aufgerufen, für das der Pfad erstellt wird. Der Rückgabewert ist entweder ein String, der ein Unterverzeichnis für das angeforderte Paket darstellt, oder None, wenn kein solches Unterverzeichnis existiert.
Die von den Importern zurückgegebenen Strings werden in der Reihenfolge, in der sie gefunden werden, zur erstellenden Pfadliste hinzugefügt. ( None-Werte und fehlende get_subpath()-Methoden werden einfach übersprungen.)
Die resultierende Liste (leer oder nicht) wird dann in einem sys.virtual_package_paths-Dictionary gespeichert, wobei die Modulnamen als Schlüssel dienen.
Dieses Dictionary hat zwei Zwecke. Erstens dient es als Cache für den Fall, dass mehr als ein Versuch unternommen wird, ein Untermodul eines virtuellen Pakets zu importieren.
Zweitens und wichtiger ist, dass das Dictionary von Code verwendet werden kann, der sys.path zur Laufzeit erweitert, um die __path__-Attribute von importierten Paketen entsprechend zu *aktualisieren*. (Siehe Standard Library Changes/Additions unten für weitere Details.)
In Python-Code würde der Algorithmus zur Erstellung virtueller Pfade etwa so aussehen
def get_virtual_path(modulename, parent_path=None):
if modulename in sys.virtual_package_paths:
return sys.virtual_package_paths[modulename]
if parent_path is None:
parent_path = sys.path
path = []
for entry in parent_path:
# Obtain a PEP 302 importer object - see pkgutil module
importer = pkgutil.get_importer(entry)
if hasattr(importer, 'get_subpath'):
subpath = importer.get_subpath(modulename)
if subpath is not None:
path.append(subpath)
sys.virtual_package_paths[modulename] = path
return path
Und eine Funktion wie diese sollte in der Standardbibliothek als z. B. imp.get_virtual_path() bereitgestellt werden, damit Personen, die __import__-Ersetzungen oder sys.meta_path-Hooks erstellen, diese wiederverwenden können.
Änderungen/Ergänzungen der Standardbibliothek
Das pkgutil-Modul sollte aktualisiert werden, um diese Spezifikation angemessen zu verarbeiten, einschließlich aller notwendigen Änderungen an extend_path(), iter_modules() usw.
Insbesondere die vorgeschlagenen Änderungen und Ergänzungen zu pkgutil sind:
- Eine neue Funktion
extend_virtual_paths(path_entry), um die__path__-Attribute bestehender, bereits importierter virtueller Pakete zu erweitern, um alle Teile einzuschließen, die in einem neuensys.path-Eintrag gefunden werden. Diese Funktion sollte von Anwendungen aufgerufen werden, diesys.pathzur Laufzeit erweitern, z. B. beim Hinzufügen eines Plugin-Verzeichnisses oder eines Eggs zum Pfad.Die Implementierung dieser Funktion führt eine einfache Top-Down-Durchquerung von
sys.virtual_package_pathsdurch und führt alle notwendigenget_subpath()-Aufrufe durch, um zu identifizieren, welche Pfadeinträge für das Paket zum virtuellen Pfad hinzugefügt werden müssen, vorausgesetzt, dasspath_entryzusys.pathhinzugefügt wurde. (Oder im Falle von Unterpaketen, Hinzufügen eines abgeleiteten Unterpfadeintrags, basierend auf dem virtuellen Pfad ihres Elternpakets.)(Hinweis: Diese Funktion muss sowohl die Pfadwerte in
sys.virtual_package_pathsals auch die__path__-Attribute der entsprechenden Module insys.modulesaktualisieren, auch wenn diese im häufigsten Fall beide dasselbelist-Objekt sind.) - Eine neue Funktion
iter_virtual_packages(parent=''), um die Top-Down-Durchquerung virtueller Pakete vonsys.virtual_package_pathszu ermöglichen, indem die Kind-Virtuellen Pakete vonparentgeliefert werden. Zum Beispiel könnte der Aufruf voniter_virtual_packages("zope")zope.appundzope.products(falls sie virtuelle Pakete sind, die insys.virtual_package_pathsaufgeführt sind) liefern, aber **nicht**zope.foo.bar. (Diese Funktion ist zur Implementierung vonextend_virtual_paths()erforderlich, ist aber auch potenziell nützlich für anderen Code, der importierte virtuelle Pakete inspizieren muss.) ImpImporter.iter_modules()sollte geändert werden, um auch Module zu erkennen und zu liefern, die in virtuellen Paketen gefunden werden.
Zusätzlich zu den oben genannten Änderungen sollte der zipimport-Importer seine iter_modules()-Implementierung ebenfalls ändern. (Hinweis: Aktuelle Python-Versionen implementieren dies über einen Shim in pkgutil, daher ist dies technisch gesehen auch eine Änderung an pkgutil.)
Zu guter Letzt sollte das imp-Modul (oder importlib, falls zutreffend) den im Abschnitt virtuelle Pfade beschriebenen Algorithmus als Funktion get_virtual_path(modulename, parent_path=None) bereitstellen, damit Ersteller von __import__-Ersetzungen sie verwenden können.
Implementierungs-Hinweise
Für Benutzer, Entwickler und Vertreiber von virtuellen Paketen
- Während virtuelle Pakete einfach einzurichten und zu verwenden sind, gibt es immer noch eine Zeit und einen Ort für die Verwendung in sich geschlossener Pakete. Obwohl es nicht unbedingt erforderlich ist, lässt die Hinzufügung eines
__init__-Moduls zu Ihren in sich geschlossenen Paketen den Benutzern des Pakets (und Python selbst) wissen, dass *der gesamte* Code des Pakets in diesem einzelnen Unterverzeichnis gefunden wird. Darüber hinaus können Sie__all__definieren, eine öffentliche API bereitstellen, eine Paket-weite Docstring bereitstellen und andere Dinge tun, die für ein in sich geschlossenes Projekt sinnvoller sind als für ein reines "Namespace"-Paket. sys.virtual_package_pathsdarf Einträge für nicht existierende oder noch nicht importierte Paketnamen enthalten; Code, der seine Inhalte verwendet, sollte nicht davon ausgehen, dass jeder Schlüssel in diesem Wörterbuch auch insys.modulesvorhanden ist oder dass der Import des Namens zwangsläufig erfolgreich ist.- Wenn Sie ein derzeit in sich geschlossenes Paket in ein virtuelles ändern, ist es wichtig zu beachten, dass Sie sein
__file__-Attribut nicht mehr verwenden können, um Datendateien zu lokalisieren, die in einem Paketverzeichnis gespeichert sind. Stattdessen müssen Sie__path__durchsuchen oder das__file__eines Untermoduls, das sich neben den gewünschten Dateien befindet, oder eines in sich geschlossenen Unterpakets, das die gewünschten Dateien enthält, verwenden.(Hinweis: Diese Einschränkung gilt bereits für bestehende Benutzer von "Namespace-Paketen". Das heißt, es ist ein inhärenter Effekt der Möglichkeit, ein Paket zu partitionieren, dass Sie wissen müssen, *in welcher* Partition sich die gewünschte Datendatei befindet. Wir erwähnen dies hier nur, damit auch *neue* Benutzer, die von in sich geschlossenen zu virtuellen Paketen konvertieren, davon Kenntnis haben.)
- XXX Was ist die __file__ eines "rein virtuellen" Pakets?
None? Ein beliebiger String? Der Pfad des ersten Verzeichnisses mit einem abschließenden Trennzeichen? Egal, was wir einfügen, *einige* Code wird kaputt gehen, aber die letzte Wahl könnte dazu führen, dass einige Code versehentlich funktionieren. Ist das gut oder schlecht?
Für diejenigen, die PEP 302 Importer-Objekte implementieren
- Importer, die die Methode
iter_modules()unterstützen (verwendet vonpkgutil, um importierbare Module und Pakete zu lokalisieren) und virtuelle Paketunterstützung hinzufügen möchten, sollten ihreiter_modules()-Methode so modifizieren, dass sie virtuelle Pakete sowie Standardmodule und -pakete erkennt und auflistet. Um dies zu tun, sollte der Importer einfach alle direkten Unterverzeichnisnamen in seinem Zuständigkeitsbereich auflisten, die gültige Python-Bezeichner sind.XXX Dies kann viele nicht-wirklich-Pakete auflisten. Müssen wir existierende importierbare Inhalte verlangen? Wenn ja, wie tief suchen wir und wie verhindern wir z. B. Link-Schleifen oder das Traversieren auf verschiedene Dateisysteme usw.? Ick. Außerdem, wenn virtuelle Pakete aufgelistet werden, können sie immer noch nicht *importiert* werden, was ein Problem für die Art und Weise darstellt, wie
pkgutil.walk_modules()derzeit implementiert ist. - "Meta"-Importer (d. h. Importer, die auf
sys.meta_pathplatziert werden) müssenget_subpath()nicht implementieren, da die Methode nur auf Importer angewendet wird, diesys.path-Einträgen und__path__-Einträgen entsprechen. Wenn ein Meta-Importer virtuelle Pakete unterstützen möchte, muss er dies vollständig innerhalb seiner eigenenfind_module()-Implementierung tun.Leider ist es unwahrscheinlich, dass eine solche Implementierung ihre Paket-Subpfade mit denen anderer Meta-Importer oder
sys.path-Importer zusammenführen kann, so dass die Bedeutung von "Unterstützung virtueller Pakete" für einen Meta-Importer derzeit undefiniert ist!(Da der vorgesehene Anwendungsfall für Meta-Importer jedoch darin besteht, den normalen Importprozess von Python für eine Teilmenge von Modulen vollständig zu ersetzen, und die Anzahl der derzeit implementierten solchen Importer recht klein ist, scheint dies in der Praxis unwahrscheinlich ein großes Problem zu sein.)
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0402.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT