Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 395 – Qualifizierte Namen für Module

Autor:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Zurückgezogen
Typ:
Standards Track
Erstellt:
04-Mrz-2011
Python-Version:
3.4
Post-History:
05-Mrz-2011, 19-Nov-2011

Inhaltsverzeichnis

Rücknahme eines PEP

Diese PEP wurde vom Autor im Dezember 2013 zurückgezogen, da andere signifikante Änderungen seit ihrer Erstellung mehrere Aspekte obsolet gemacht haben. Insbesondere haben PEP 420 Namespace-Pakete einige der Vorschläge zur Paketenerkennung unbrauchbar gemacht und PEP 451 Modul-Spezifikationen haben die Multiprocessing-Probleme gelöst und eine mögliche Möglichkeit zur Bewältigung der Pickle-Kompatibilitätsprobleme geboten.

Eine zukünftige PEP zur Lösung der verbleibenden Probleme wäre angemessen, aber es ist ratsam, eine solche Anstrengung als neue PEP zu beginnen, die die verbleibenden Probleme in einem aktualisierten Kontext darlegt, anstatt direkt auf dieser aufzubauen.

Zusammenfassung

Diese PEP schlägt neue Mechanismen vor, die einige langjährige Fallen für Unerfahrene beim Umgang mit Pythons Import-System sowie Serialisierung und Introspektion von Funktionen und Klassen beseitigen.

Sie baut auf dem Konzept des „Qualifizierten Namens“ auf, das in PEP 3155 definiert wurde.

Beziehung zu anderen PEPs

Am wichtigsten ist, dass diese PEP derzeit zurückgestellt wird, da sie erhebliche Änderungen erfordert, um mit der Entfernung von obligatorischen `__init__.py`-Dateien in PEP 420 (die implementiert und in Python 3.3 veröffentlicht wurde) kompatibel zu sein.

Diese PEP baut auf dem von PEP 3155 eingeführten Konzept des „qualifizierten Namens“ auf und teilt auch das Ziel dieser PEP, einige unschöne Eckfälle beim Umgang mit der Serialisierung beliebiger Funktionen und Klassen zu beheben.

Sie baut auch auf PEP 366 auf, die erste vorsichtige Schritte unternahm, um explizite relative Importe aus dem Hauptmodul unter *einigen* Umständen korrekt funktionieren zu lassen.

Schließlich eliminierte PEP 328 implizite relative Importe aus importierten Modulen. Diese PEP schlägt vor, dass die de facto impliziten relativen Importe aus Hauptmodulen, die durch das aktuelle Initialisierungsverhalten für sys.path[0] bereitgestellt werden, ebenfalls eliminiert werden.

Was steckt in einem __name__?

Im Laufe der Zeit wurde das Attribut __name__ eines Moduls zur Handhabung einer Reihe verschiedener Aufgaben verwendet.

Die wichtigsten identifizierten Anwendungsfälle für dieses Modulattribut sind:

  1. Markierung des Hauptmoduls in einem Programm mit der Konvention if __name__ == "__main__":.
  2. Als Ausgangspunkt für relative Importe
  3. Zur Identifizierung des Speicherorts von Funktions- und Klassendefinitionen innerhalb der laufenden Anwendung
  4. Zur Identifizierung des Speicherorts von Klassen für die Serialisierung in Pickle-Objekte, die mit anderen Interpreter-Instanzen geteilt werden können

Fallen für Unerfahrene

Die Überladung der Semantik von __name__, zusammen mit einigen historisch damit verbundenen Verhaltensweisen bei der Initialisierung von sys.path[0], hat zu mehreren Fallen für Unerfahrene geführt. Diese Fallen können in der Praxis sehr störend sein, da sie höchst undurchsichtig (insbesondere für Anfänger) sind und sehr verwirrendes Verhalten verursachen können.

Warum sind meine Importe fehlerhaft?

Es gibt ein allgemeines Prinzip, das beim Ändern von sys.path gilt: *Niemals* ein Paketverzeichnis direkt auf sys.path legen. Der Grund, warum dies problematisch ist, liegt darin, dass jedes Modul in diesem Verzeichnis nun potenziell unter zwei verschiedenen Namen zugänglich ist: als Top-Level-Modul (da das Paketverzeichnis auf sys.path liegt) und als Untermodul des Pakets (wenn das übergeordnete Verzeichnis, das das Paket selbst enthält, ebenfalls auf sys.path liegt).

Als Beispiel ist Django (bis einschließlich Version 1.3) schuldig, genau diese Situation für standortspezifische Anwendungen einzurichten – die Anwendung ist dann als app und site.app im Modul-Namensraum zugänglich, und dies sind tatsächlich zwei *unterschiedliche* Kopien des Moduls. Dies ist ein Rezept für Verwirrung, wenn es irgendeinen sinnvollen veränderlichen Modul-zustand gibt, daher wird dieses Verhalten in Version 1.4 aus der Standard-Site-Einrichtung entfernt (standortspezifische Apps werden immer vollständig mit dem Standortnamen qualifiziert).

Es ist jedoch schwer, Django dafür zu beschuldigen, wenn derselbe Teil von Python, der für die Einstellung von __name__ = "__main__" im Hauptmodul verantwortlich ist, denselben Fehler begeht, wenn er den Wert für sys.path[0] bestimmt.

Die Auswirkungen davon sind relativ häufig zu sehen, wenn man den Tags „python“ und „import“ auf Stack Overflow folgt. Als ich Zeit hatte, dem selbst zu folgen, stieß ich regelmäßig auf Leute, die Schwierigkeiten hatten, das Verhalten einfacher Paketlayouts wie des folgenden zu verstehen (ich verwende tatsächlich Paketlayouts dieser Art in meinen eigenen Projekten)

project/
    setup.py
    example/
        __init__.py
        foo.py
        tests/
            __init__.py
            test_foo.py

Während ich es anfangs oft ohne die `__init__.py`-Dateien sah, ist das eine triviale Korrektur zu erklären. Was schwer zu erklären ist, ist, dass alle folgenden Wege, um `test_foo.py` aufzurufen, *wahrscheinlich nicht funktionieren* werden, wegen fehlerhafter Importe (entweder wird `example` für absolute Importe nicht gefunden, es wird über relative Importe in einem Nicht-Paket oder außerhalb des Top-Level-Pakets für explizite relative Importe geklagt, oder es werden noch obskurere Fehler ausgegeben, wenn ein anderes Untermodul zufällig den Namen eines Top-Level-Moduls überschattet, wie z.B. ein `example.json`-Modul, das die Serialisierung handhabt, oder ein `example.tests.unittest`-Testrunner)

# These commands will most likely *FAIL*, even if the code is correct

# working directory: project/example/tests
./test_foo.py
python test_foo.py
python -m package.tests.test_foo
python -c "from package.tests.test_foo import main; main()"

# working directory: project/package
tests/test_foo.py
python tests/test_foo.py
python -m package.tests.test_foo
python -c "from package.tests.test_foo import main; main()"

# working directory: project
example/tests/test_foo.py
python example/tests/test_foo.py

# working directory: project/..
project/example/tests/test_foo.py
python project/example/tests/test_foo.py
# The -m and -c approaches don't work from here either, but the failure
# to find 'package' correctly is easier to explain in this case

Das stimmt, diese lange Liste sind alle Methoden der Ausführung, die fast sicher *fehlschlagen* werden, wenn Sie sie versuchen, und die Fehlermeldungen werden keinen Sinn ergeben, wenn Sie nicht bereits tief mit der Funktionsweise des Python-Importsystems vertraut sind, sondern auch damit, wie es initialisiert wird.

Lange Zeit war der einzige Weg, sys.path mit dieser Art von Einrichtung richtig hinzubekommen, entweder es manuell in test_foo.py selbst festzulegen (kaum etwas, das ein Anfänger, oder auch viele erfahrene Python-Programmierer, wissen wird, wie es geht) oder sicherzustellen, dass das Modul importiert statt direkt ausgeführt wird.

# working directory: project
python -c "from package.tests.test_foo import main; main()"

Seit der Implementierung von PEP 366 (die einen Mechanismus definierte, der es ermöglicht, dass relative Importe korrekt funktionieren, wenn ein Modul innerhalb eines Pakets über den `-m`-Schalter ausgeführt wird), funktioniert auch das Folgende korrekt.

# working directory: project
python -m package.tests.test_foo

Die Tatsache, dass die meisten Methoden, Python-Code von der Kommandozeile aus aufzurufen, fehlschlagen, wenn sich dieser Code in einem Paket befindet, und die beiden, die funktionieren, stark vom aktuellen Arbeitsverzeichnis abhängen, ist für Anfänger völlig verwirrend. Ich persönlich glaube, dass dies einer der Schlüsselfaktoren ist, die zu der Wahrnehmung führen, dass Python-Pakete kompliziert und schwierig zu handhaben sind.

Dieses Problem beschränkt sich nicht einmal auf die Kommandozeile – wenn test_foo.py in Idle geöffnet ist und Sie versuchen, es durch Drücken von F5 auszuführen, oder wenn Sie versuchen, es durch Klicken darauf in einem grafischen Dateimanager auszuführen, wird es auf die gleiche Weise fehlschlagen, wie wenn es direkt von der Kommandozeile ausgeführt würde.

Es gibt einen Grund, warum die allgemeine Richtlinie „keine Paketverzeichnisse auf sys.path“ existiert, und die Tatsache, dass der Interpreter selbst sie bei der Bestimmung von sys.path[0] nicht befolgt, ist die Wurzel aller Probleme.

In der Vergangenheit konnte dies aufgrund von Bedenken hinsichtlich der Abwärtskompatibilität nicht behoben werden. Skripte, die potenziell von diesem Problem betroffen sind, benötigen jedoch bereits Korrekturen bei der Portierung auf Python 3.x (aufgrund der Eliminierung impliziter relativer Importe beim normalen Importieren von Modulen). Dies bietet eine bequeme Gelegenheit, eine entsprechende Änderung in den Initialisierungssemantik für sys.path[0] zu implementieren.

Das Hauptmodul zweimal importieren

Eine weitere ehrwürdige Falle ist das Problem des zweifachen Imports von `__main__`. Dies tritt auf, wenn das Hauptmodul auch unter seinem echten Namen importiert wird, was effektiv zwei Instanzen desselben Moduls unter verschiedenen Namen erzeugt.

Wenn der in `__main__` gespeicherte Zustand für den korrekten Betrieb des Programms wichtig ist oder wenn es Code auf oberster Ebene im Hauptmodul gibt, der nicht-idempotente Nebenwirkungen hat, kann diese Duplizierung obskure und überraschende Fehler verursachen.

In der Klemme

Was viele Benutzer möglicherweise nicht realisieren, ist, dass das `pickle`-Modul manchmal das `__module__`-Attribut verwendet, wenn Instanzen beliebiger Klassen serialisiert werden. Wenn also Instanzen von Klassen, die in `__main__` definiert sind, auf diese Weise gepickelt werden, werden sie von einer anderen Python-Instanz, die dieses Modul nur importiert statt es direkt auszuführen, nicht korrekt entpickelt. Dieses Verhalten ist der zugrundeliegende Grund für den Rat vieler Python-Veteranen, in jeder Anwendung, die irgendeine Form von Objektserialisierung und -persistenz beinhaltet, so wenig wie möglich im `__main__`-Modul zu tun.

Ebenso verlassen sich Pickles bei der Erstellung eines Pseudo-Moduls (siehe nächster Absatz) auf den Namen des Moduls, in dem eine Klasse tatsächlich definiert ist, anstatt auf den offiziell dokumentierten Speicherort dieser Klasse in der Modulhierarchie.

Für die Zwecke dieser PEP ist ein „Pseudo-Modul“ ein Paket, das wie die Python 3.2 `unittest`- und `concurrent.futures`-Pakete konzipiert ist. Diese Pakete werden so dokumentiert, als wären sie einzelne Module, sind aber intern tatsächlich als Paket implementiert. Dies ist *gedacht* als Implementierungsdetail, um das sich Benutzer und andere Implementierungen keine Sorgen machen müssen, aber dank `pickle` (und Serialisierung im Allgemeinen) werden die Details oft offengelegt und können effektiv Teil der öffentlichen API werden.

Während sich diese PEP speziell auf `pickle` als primäres Serialisierungsschema in der Standardbibliothek konzentriert, kann dieses Problem auch andere Mechanismen betreffen, die die Serialisierung beliebiger Klasseninstanzen unterstützen und `__module__`-Attribute verwenden, um zu bestimmen, wie die Deserialisierung gehandhabt werden soll.

Wo ist der Quelltext?

Einige fortgeschrittene Benutzer der oben beschriebenen Pseudo-Modul-Technik erkennen das Problem mit Implementierungsdetails, die über das `pickle`-Modul nach außen dringen, und entscheiden sich, es anzugehen, indem sie `__name__` auf den öffentlichen Speicherort für das Modul ändern, bevor sie Funktionen oder Klassen definieren (oder indem sie die `__module__`-Attribute dieser Objekte nach ihrer Definition ändern).

Dieser Ansatz ist effektiv, um das Austreten von Informationen über Pickling zu verhindern, geht aber auf Kosten der Beschädigung der Introspektion für Funktionen und Klassen (da ihr `__module__`-Attribut nun auf den falschen Speicherort verweist).

Forkless Windows

Um den Mangel an `os.fork` unter Windows zu umgehen, versucht das `multiprocessing`-Modul, Python mit demselben Hauptmodul neu auszuführen, überspringt aber Code, der von `if __name__ == "__main__":`-Prüfungen geschützt wird. Es tut sein Bestes mit den Informationen, die es hat, ist aber gezwungen, Annahmen zu treffen, die einfach nicht gültig sind, wann immer das Hauptmodul kein gewöhnliches, direkt ausgeführtes Skript oder Top-Level-Modul ist. Pakete und nicht-Top-Level-Module, die über den `-m`-Schalter ausgeführt werden, sowie direkt ausgeführte Zip-Dateien oder Verzeichnisse, lassen Multiprocessing unter Windows wahrscheinlich das Falsche tun (entweder stillschweigend oder geräuschvoll, je nach Anwendungsdetails), wenn ein neuer Prozess gestartet wird.

Während dieses Problem derzeit nur Windows direkt betrifft, wirkt es sich auch auf alle Vorschläge aus, Windows-ähnliche „Clean Process“-Aufrufe über das Multiprocessing-Modul auf anderen Plattformen bereitzustellen.

Qualifizierte Namen für Module

Um die endgültige Behebung dieser Probleme zu ermöglichen, wird vorgeschlagen, ein neues Modulattribut hinzuzufügen: `__qualname__`. Diese Abkürzung von „qualifizierter Name“ stammt aus PEP 3155, wo es verwendet wird, um den Namenspfad zu einer verschachtelten Klassen- oder Funktionsdefinition relativ zum Top-Level-Modul zu speichern.

Für Module ist `__qualname__` normalerweise dasselbe wie `__name__`, genau wie bei Top-Level-Funktionen und Klassen in PEP 3155. Es wird jedoch in einigen Situationen abweichen, damit die oben genannten Probleme behoben werden können.

Insbesondere, wann immer `__name__` für einen anderen Zweck geändert wird (z. B. zur Bezeichnung des Hauptmoduls), bleibt `__qualname__` unverändert, sodass Code, der es benötigt, auf den ursprünglichen unveränderten Wert zugreifen kann.

Wenn ein Modul-Loader `__qualname__` nicht selbst initialisiert, fügt das Import-System es automatisch hinzu (setzt es auf denselben Wert wie `__name__`).

Alternative Namen

Zwei alternative Namen wurden ebenfalls für das neue Attribut in Betracht gezogen: „full name“ (`__fullname__`) und „implementation name“ (`__implname__`).

Jeder dieser Namen wäre für den Anwendungsfall in dieser PEP tatsächlich gültig. Als Metathema fügt PEP 3155 jedoch *auch* ein neues Attribut hinzu (für Funktionen und Klassen), das „wie `__name__` ist, aber in einigen Fällen anders, in denen `__name__` notwendige Informationen fehlen“ und diese Begriffe für den Funktions- und Klassenanwendungsfall von PEP 3155 nicht korrekt sind.

PEP 3155 lässt die Modulinformationen bewusst weg, daher ist der Begriff „full name“ einfach falsch, und „implementation name“ impliziert, dass er ein anderes Objekt bezeichnen könnte als das von `__name__` spezifizierte, und das ist bei PEP 3155 niemals der Fall (in dieser PEP beziehen sich `__name__` und `__qualname__` immer auf dieselbe Funktion oder Klasse, es ist nur so, dass `__name__` nicht ausreicht, um verschachtelte Funktionen und Klassen korrekt zu identifizieren).

Da es als unnötig inkonsistent erscheint, *zwei* neue Begriffe für Attribute hinzuzufügen, die nur existieren, weil Bedenken hinsichtlich der Abwärtskompatibilität uns daran hindern, das Verhalten von `__name__` selbst zu ändern, hat diese PEP stattdessen die Terminologie von PEP 3155 übernommen.

Wenn die relative Undurchsichtigkeit von „qualified name“ und `__qualname__` interessierte Entwickler dazu anregt, sie zumindest einmal nachzuschlagen, anstatt anzunehmen, sie wüssten, was sie bedeuten, nur anhand des Namens und falsch zu raten, ist das nicht unbedingt ein schlechtes Ergebnis.

Außerdem sollten 99 % der Python-Entwickler nie wissen müssen, dass diese zusätzlichen Attribute existieren – sie sind wirklich ein Implementierungsdetail, das es uns ermöglicht, einige problematische Verhaltensweisen von Importen, Pickling und Introspektion zu beheben, nicht etwas, womit Leute regelmäßig zu tun haben werden.

Beseitigung der Fallen

Die folgenden Änderungen sind miteinander verknüpft und ergeben am meisten Sinn, wenn sie zusammen betrachtet werden. Sie beseitigen gemeinsam entweder vollständig die oben genannten Fallen für Unerfahrene oder bieten einfache Mechanismen zu deren Bewältigung.

Ein grober Entwurf einiger hier vorgestellter Konzepte wurde erstmals auf der python-ideas-Liste veröffentlicht ([1]), hat sich aber seit der ersten Diskussion in diesem Thread erheblich weiterentwickelt. Weitere Diskussionen fanden anschließend auf der import-sig Mailingliste statt ([2]. [3]).

Behebung von Hauptmodul-Importen innerhalb von Paketen

Um diese Falle zu beseitigen, wird vorgeschlagen, eine zusätzliche Dateisystemprüfung durchzuführen, wenn ein geeigneter Wert für sys.path[0] bestimmt wird. Diese Prüfung sucht nach expliziten Paketverzeichnis-Markern von Python und verwendet diese, um das entsprechende Verzeichnis zu finden, das zu sys.path hinzugefügt werden soll.

Der aktuelle Algorithmus zum Setzen von sys.path[0] in relevanten Fällen ist ungefähr wie folgt:

# Interactive prompt, -m switch, -c switch
sys.path.insert(0, '')
# Valid sys.path entry execution (i.e. directory and zip execution)
sys.path.insert(0, sys.argv[0])
# Direct script execution
sys.path.insert(0, os.path.dirname(sys.argv[0]))

Es wird vorgeschlagen, diesen Initialisierungsprozess so zu modifizieren, dass Paketdetails, die auf dem Dateisystem gespeichert sind, berücksichtigt werden.

# Interactive prompt, -m switch, -c switch
in_package, path_entry, _ignored = split_path_module(os.getcwd(), '')
if in_package:
    sys.path.insert(0, path_entry)
else:
    sys.path.insert(0, '')

# Start interactive prompt or run -c command as usual
#   __main__.__qualname__ is set to "__main__"

# The -m switches uses the same sys.path[0] calculation, but:
#   modname is the argument to the -m switch
#   modname is passed to ``runpy._run_module_as_main()`` as usual
#   __main__.__qualname__ is set to modname
# Valid sys.path entry execution (i.e. directory and zip execution)
modname = "__main__"
path_entry, modname = split_path_module(sys.argv[0], modname)
sys.path.insert(0, path_entry)

# modname (possibly adjusted) is passed to ``runpy._run_module_as_main()``
# __main__.__qualname__ is set to modname
# Direct script execution
in_package, path_entry, modname = split_path_module(sys.argv[0])
sys.path.insert(0, path_entry)
if in_package:
    # Pass modname to ``runpy._run_module_as_main()``
else:
    # Run script directly
# __main__.__qualname__ is set to modname

Die unterstützende Funktion `split_path_module()` im obigen Pseudocode hätte die folgenden Semantiken:

def _splitmodname(fspath):
    path_entry, fname = os.path.split(fspath)
    modname = os.path.splitext(fname)[0]
    return path_entry, modname

def _is_package_dir(fspath):
    return any(os.exists("__init__" + info[0]) for info
                   in imp.get_suffixes())

def split_path_module(fspath, modname=None):
    """Given a filesystem path and a relative module name, determine an
       appropriate sys.path entry and a fully qualified module name.

       Returns a 3-tuple of (package_depth, fspath, modname). A reported
       package depth of 0 indicates that this would be a top level import.

       If no relative module name is given, it is derived from the final
       component in the supplied path with the extension stripped.
    """
    if modname is None:
        fspath, modname = _splitmodname(fspath)
    package_depth = 0
    while _is_package_dir(fspath):
        fspath, pkg = _splitmodname(fspath)
        modname = pkg + '.' + modname
    return package_depth, fspath, modname

Diese PEP schlägt auch vor, dass die `split_path_module()`-Funktionalität direkt für Python-Benutzer über das `runpy`-Modul zugänglich gemacht wird.

Mit dieser Korrektur und dem gleichen einfachen Paketlayout wie zuvor beschrieben, würden *alle* folgenden Befehle die Testsuite korrekt aufrufen:

# working directory: project/example/tests
./test_foo.py
python test_foo.py
python -m package.tests.test_foo
python -c "from .test_foo import main; main()"
python -c "from ..tests.test_foo import main; main()"
python -c "from package.tests.test_foo import main; main()"

# working directory: project/package
tests/test_foo.py
python tests/test_foo.py
python -m package.tests.test_foo
python -c "from .tests.test_foo import main; main()"
python -c "from package.tests.test_foo import main; main()"

# working directory: project
example/tests/test_foo.py
python example/tests/test_foo.py
python -m package.tests.test_foo
python -c "from package.tests.test_foo import main; main()"

# working directory: project/..
project/example/tests/test_foo.py
python project/example/tests/test_foo.py
# The -m and -c approaches still don't work from here, but the failure
# to find 'package' correctly is pretty easy to explain in this case

Mit diesen Änderungen sollte das Klicken auf Python-Module in einem grafischen Dateimanager diese immer korrekt ausführen, auch wenn sie sich in einem Paket befinden. Abhängig von den Details der Skriptaufrufe sollte Idle `test_foo.py` mit F5 wahrscheinlich auch korrekt ausführen können, ohne dass Idle-spezifische Korrekturen erforderlich sind.

Optionale Ergänzung: Relative Importe über die Kommandozeile

Mit den oben genannten Änderungen wäre es eine relativ geringfügige Ergänzung, explizite relative Importe als Argumente für den `-m`-Schalter zu erlauben.

# working directory: project/example/tests
python -m .test_foo
python -m ..tests.test_foo

# working directory: project/example/
python -m .tests.test_foo

Mit dieser Ergänzung würden die Systemeinstellungen für den `-m`-Schalter wie folgt geändert:

# -m switch (permitting explicit relative imports)
in_package, path_entry, pkg_name = split_path_module(os.getcwd(), '')
qualname= <<arguments to -m switch>>
if qualname.startswith('.'):
    modname = qualname
    while modname.startswith('.'):
        modname = modname[1:]
        pkg_name, sep, _ignored = pkg_name.rpartition('.')
        if not sep:
            raise ImportError("Attempted relative import beyond top level package")
    qualname = pkg_name + '.' modname
if in_package:
    sys.path.insert(0, path_entry)
else:
    sys.path.insert(0, '')

# qualname is passed to ``runpy._run_module_as_main()``
# _main__.__qualname__ is set to qualname

Kompatibilität mit PEP 382

Die Kompatibilität mit der PEP 382 zur Namespace-Paketierung ist trivial. Die Semantik von `_is_package_dir()` wird lediglich zu folgender geändert:

def _is_package_dir(fspath):
    return (fspath.endswith(".pyp") or
            any(os.exists("__init__" + info[0]) for info
                    in imp.get_suffixes()))

Inkompatibilität mit PEP 402

PEP 402 schlägt die Eliminierung expliziter Markierungen im Dateisystem für Python-Pakete vor. Dies untergräbt grundlegend das vorgeschlagene Konzept, einen Dateisystempfad und einen Python-Modulnamen nehmen zu können und eine eindeutige Zuordnung zum Python-Modul-Namensraum zu ermitteln. Stattdessen würde die geeignete Zuordnung von den aktuellen Werten in `sys.path` abhängen, was es unmöglich macht, die oben beschriebenen Probleme bei der Berechnung von `sys.path[0]` bei der Initialisierung des Interpreters jemals zu beheben.

Während einige Aspekte dieser PEP wahrscheinlich gerettet werden könnten, wenn PEP 402 übernommen würde, wäre das Kernkonzept der Konsistenz der Importsemantik von Haupt- und anderen Modulen nicht mehr machbar.

Diese Inkompatibilität wird in den relevanten import-sig Threads ([2], [3]) detaillierter diskutiert.

Potenzielle Inkompatibilitäten mit Skripten, die in Paketen gespeichert sind

Die vorgeschlagene Änderung der Initialisierung von sys.path[0] *kann* einige bestehende Codes brechen. Insbesondere werden Skripte in Paketverzeichnissen, die auf die impliziten relativen Importe von __main__ angewiesen sind, um unter Python 3 korrekt zu laufen, gebrochen.

Während solche Skripte in Python 2 importiert werden konnten (wegen impliziter relativer Importe), ist es bereits der Fall, dass sie in Python 3 nicht importiert werden können, da implizite relative Importe nicht mehr erlaubt sind, wenn ein Modul importiert wird.

Indem implizite relative Importe auch aus dem Hauptmodul nicht mehr zugelassen werden, funktionieren solche Module mit dieser PEP nicht einmal mehr als Skripte. Die Umstellung auf explizite relative Importe bringt sie dann wieder sowohl als ausführbare Skripte als auch als importierbare Module zum Laufen.

Um frühere Python-Versionen zu unterstützen, könnte ein Skript geschrieben werden, das je nach Python-Version unterschiedliche Importformen verwendet.

if __name__ == "__main__" and sys.version_info < (3, 3):
    import peer # Implicit relative import
else:
    from . import peer # explicit relative import

Behebung von doppelten Importen des Hauptmoduls

Angesichts des oben genannten Vorschlags, `__qualname__` im Hauptmodul konsistent korrekt einzustellen, wird eine einfache Änderung vorgeschlagen, um das Problem der doppelten Importe des Hauptmoduls zu beheben: die Hinzufügung eines `sys.metapath`-Hooks, der Versuche, `__main__` unter seinem echten Namen zu importieren, erkennt und stattdessen das ursprüngliche Hauptmodul zurückgibt.

class AliasImporter:
  def __init__(self, module, alias):
      self.module = module
      self.alias = alias

  def __repr__(self):
      fmt = "{0.__class__.__name__}({0.module.__name__}, {0.alias})"
      return fmt.format(self)

  def find_module(self, fullname, path=None):
      if path is None and fullname == self.alias:
          return self
      return None

  def load_module(self, fullname):
      if fullname != self.alias:
          raise ImportError("{!r} cannot load {!r}".format(self, fullname))
      return self.main_module

Dieser Metapath-Hook würde bei der Initialisierung des Importsystems basierend auf der folgenden Logik automatisch hinzugefügt:

main = sys.modules["__main__"]
if main.__name__ != main.__qualname__:
    sys.metapath.append(AliasImporter(main, main.__qualname__))

Dies ist wahrscheinlich der am wenigsten wichtige Vorschlag in der PEP – er schließt nur den letzten Mechanismus ab, der wahrscheinlich zu Modulduplizierung führt, nachdem die Konfiguration von `sys.path[0]` beim Interpreterstart behoben wurde.

Behebung von Pickling ohne Beeinträchtigung der Introspektion

Um dieses Problem zu beheben, wird vorgeschlagen, die neuen Modul-Level-Attribute `__qualname__` zu verwenden, um den tatsächlichen Modulort zu bestimmen, wenn `__name__` aus irgendeinem Grund geändert wurde.

Im Hauptmodul wird `__qualname__` automatisch vom Interpreter auf den „echten“ Namen des Hauptmoduls gesetzt (wie oben beschrieben).

Pseudo-Module, die `__name__` auf den öffentlichen Namensraum setzen, lassen `__qualname__` unverändert, sodass der Implementierungsort für die Introspektion leicht zugänglich bleibt.

Wenn `__name__` am Anfang eines Moduls geändert wird, wird dadurch auch das `__module__`-Attribut für alle anschließend in diesem Modul definierten Funktionen und Klassen geändert.

Da mehrere Untermodule so eingestellt werden können, dass sie denselben „öffentlichen“ Namensraum verwenden, erhalten Funktionen und Klassen ein neues `__qualmodule__`-Attribut, das auf das `__qualname__` ihres Moduls verweist.

Dies ist für Funktionen nicht unbedingt erforderlich (man könnte ihren Modul-qualifizierten Namen durch Nachschlagen im globalen Dictionary ermitteln), ist aber für Klassen notwendig, da sie keine Referenz auf die Globals ihres definierenden Moduls enthalten. Sobald ein neues Attribut zu Klassen hinzugefügt wird, ist es bequemer, die API konsistent zu halten und auch ein neues Attribut zu Funktionen hinzuzufügen.

Diese Änderungen bedeuten, dass das Ändern von `__name__` (und entweder direkt oder indirekt der entsprechenden Funktions- und Klassen-`__module__`-Attribute) zum offiziell genehmigten Weg wird, einen Namensraum als Paket zu implementieren, während die API so präsentiert wird, als wäre sie immer noch ein einzelnes Modul.

Der gesamte Serialisierungscode, der derzeit die Attribute `__name__` und `__module__` verwendet, vermeidet dann standardmäßig die Offenlegung von Implementierungsdetails.

Um die Serialisierung von Elementen aus dem Hauptmodul korrekt zu handhaben, werden die Logik für Klassen- und Funktionsdefinitionen aktualisiert, um auch `__qualname__` für das `__module__`-Attribut zu verwenden, wenn `__name__ == "__main__".

Da `__name__` und `__module__` offiziell als Namen für die *öffentlichen* Namen von Dingen gesegnet sind, werden die Introspektionswerkzeuge in der Standardbibliothek aktualisiert, um `__qualname__` und `__qualmodule__` dort zu verwenden, wo es angebracht ist. Zum Beispiel:

  • `pydoc` wird sowohl öffentliche als auch qualifizierte Namen für Module melden.
  • `inspect.getsource()` (und ähnliche Werkzeuge) verwenden die qualifizierten Namen, die auf die Implementierung des Codes verweisen.
  • Es können zusätzliche `pydoc`- und/oder `inspect`-APIs bereitgestellt werden, die alle Module mit einem bestimmten öffentlichen `__name__` melden.

Behebung von Multiprocessing unter Windows

Mit `__qualname__` kann `multiprocessing` nun den echten Namen des Hauptmoduls erfahren und ihn einfach in die an den Kindprozess übergebenen serialisierten Informationen aufnehmen, wodurch die derzeit zweifelhafte Introspektion des `__file__`-Attributs überflüssig wird.

Für ältere Python-Versionen könnte `multiprocessing` verbessert werden, indem der oben beschriebene `split_path_module()`-Algorithmus angewendet wird, wenn versucht wird, zu ermitteln, wie das Hauptmodul basierend auf seinem `__file__`-Attribut ausgeführt werden soll.

Explizite relative Importe

Diese PEP schlägt vor, dass `__package__` im Hauptmodul bedingungslos als `__qualname__.rpartition('.')[0]` definiert wird. Davon abgesehen schlägt sie vor, das Verhalten von expliziten relativen Importen unverändert zu lassen.

Insbesondere, wenn `__package__` in einem Modul nicht gesetzt ist, wenn ein expliziter relativer Import auftritt, wird der automatisch zwischengespeicherte Wert weiterhin von `__name__` anstatt von `__qualname__` abgeleitet. Dies minimiert Rückwärtskompatibilitätsprobleme mit vorhandenem Code, der explizit relative Importe manipuliert, indem er `__name__` ändert, anstatt `__package__` direkt zu setzen.

Diese PEP schlägt *nicht* vor, `__package__` zu verwerfen. Obwohl es technisch redundant ist nach der Einführung von `__qualname__`, lohnt es sich nicht, es während der Lebensdauer von Python 3.x zu verwerfen.

Referenzimplementierung

Bisher keine.

Referenzen


Quelle: https://github.com/python/peps/blob/main/peps/pep-0395.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT