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

Python Enhancement Proposals

PEP 3122 – Abgrenzung des Hauptmoduls

Autor:
Brett Cannon
Status:
Abgelehnt
Typ:
Standards Track
Erstellt:
27-Apr-2007
Post-History:


Inhaltsverzeichnis

Aufmerksamkeit

Diese PEP wurde abgelehnt. Guido betrachtet das Ausführen von Skripten innerhalb eines Pakets als Anti-Pattern [3].

Zusammenfassung

Aufgrund der Funktionsweise der Namensauflösung für relative Importe in einer Welt, in der PEP 328 implementiert ist, ist die Möglichkeit, Module innerhalb eines Pakets auszuführen, nicht mehr gegeben. Dieses Versagen beruht darauf, dass das als "Haupt"-Modul ausgeführte Modul sein __name__ Attribut mit "__main__" überschreibt, anstatt es als absoluten Namen des Moduls zu belassen. Dies unterbricht die Fähigkeit des Importmechanismus, relative Importe vom Hauptmodul in absolute Namen aufzulösen.

Um dieses Problem zu lösen, schlägt diese PEP vor, die Art und Weise zu ändern, wie das Hauptmodul abgegrenzt wird. Indem das __name__ Attribut in einem Modul unverändert bleibt und sys.main auf den Namen des Hauptmoduls gesetzt wird, wird es zumindest in einigen Fällen möglich sein, ein Modul innerhalb eines Pakets auszuführen, das relative Importe verwendet.

Diese PEP befasst sich nicht mit der Idee, eine Funktion auf Modulebene einzuführen, die automatisch ausgeführt wird, wie es PEP 299 vorschlägt.

Das Problem

Mit der Einführung von PEP 328 wurden relative Importe vom __name__ Attribut des Moduls, das den Import durchführt, abhängig. Dies liegt daran, dass die Verwendung von Punkten in einem relativen Import verwendet wird, um Teile des Namens des aufrufenden Moduls zu entfernen und zu berechnen, wo in der Paket-Hierarchie ein Import fallen soll (vor PEP 328 konnten relative Importe fehlschlagen und fielen auf absolute Importe zurück, die eine Erfolgschance hatten).

Betrachten Sie zum Beispiel den Import from .. import spam, der aus dem Modul bacon.ham.beans gemacht wird (bacon.ham.beans ist selbst kein Paket, d. h. es definiert kein __path__). Die Namensauflösung des relativen Imports nimmt den Namen des Aufrufers (bacon.ham.beans), teilt ihn an Punkten auf und schneidet dann die letzten n Teile basierend auf dem Level (was 2 ist) ab. In diesem Beispiel werden sowohl ham als auch beans verworfen und spam mit dem, was übrig bleibt (bacon), verbunden. Dies führt zum korrekten Import des Moduls bacon.spam.

Diese Abhängigkeit vom __name__ Attribut eines Moduls bei der Verarbeitung relativer Importe wird zu einem Problem, wenn ein Skript innerhalb eines Pakets ausgeführt wird. Da der Name des ausgeführten Skripts auf '__main__' gesetzt ist, kann der Import keine relativen Importe auflösen, was zu einem ImportError führt.

Nehmen Sie zum Beispiel ein Paket namens bacon mit einer Datei __init__.py, die Folgendes enthält:

from . import spam

Erstellen Sie außerdem ein Modul namens spam innerhalb des Pakets bacon (es kann eine leere Datei sein). Wenn Sie nun versuchen, das Paket bacon auszuführen (entweder über python bacon/__init__.py oder python -m bacon), erhalten Sie einen ImportError wegen des Versuchs eines relativen Imports von innerhalb eines Nicht-Pakets. Offensichtlich ist der Import gültig, aber aufgrund der Einstellung von __name__ auf '__main__' denkt der Importmechanismus, dass bacon/__init__.py nicht in einem Paket liegt, da keine Punkte in __name__ vorhanden sind. Um zu sehen, wie der Algorithmus im Detail funktioniert, siehe importlib.Import._resolve_name() im Sandbox-Bereich [2].

Derzeit ist eine Umgehungslösung, alle relativen Importe im auszuführenden Modul zu entfernen und sie absolut zu machen. Das ist jedoch bedauerlich, da man keine spezifische Art von Ressource verwenden sollte, um ein Modul in einem Paket ausführbar zu machen.

Die Lösung

Die Lösung des Problems besteht darin, den Wert von __name__ in Modulen nicht zu ändern. Es muss jedoch weiterhin eine Möglichkeit geben, dem ausführenden Code mitzuteilen, dass er als Skript ausgeführt wird. Dies wird durch ein neues Attribut im sys Modul namens main gehandhabt.

Wenn ein Modul als Skript ausgeführt wird, wird sys.main auf den Namen des Moduls gesetzt. Dies ändert das aktuelle Idiom von

if __name__ == '__main__':
    ...

zu

import sys
if __name__ == sys.main:
    ...

Die neu vorgeschlagene Lösung führt zwar eine zusätzliche Zeile Boilerplate ein, die ein Modulimport ist. Aber da die Lösung keine neue Built-in-Funktion oder kein neues Modulattribut einführt (wie in Abgelehnte Ideen diskutiert), wurde sie als die zusätzliche Zeile wert erachtet.

Ein weiteres Problem mit dem vorgeschlagenen Lösungsansatz (das auch für alle abgelehnten Ideen gilt) ist, dass es das Problem der Ermittlung des Dateinamens nicht direkt löst. Betrachten Sie python bacon/spam.py. Allein aus dem Dateinamen ist nicht ersichtlich, ob bacon ein Paket ist. Um dies richtig festzustellen, müssen sowohl die aktuelle Richtung auf sys.path vorhanden sein als auch bacon/__init__.py existieren.

Aber das ist das einfache Beispiel. Betrachten Sie python ../spam.py. Allein aus dem Dateinamen ist nicht klar, ob spam.py in einem Paket ist oder nicht. Eine mögliche Lösung besteht darin, herauszufinden, wie der absolute Name von .. lautet, zu prüfen, ob eine Datei namens __init__.py existiert, und dann zu prüfen, ob das Verzeichnis auf sys.path liegt. Wenn dies nicht der Fall ist, wird weiterhin im Verzeichnis aufgestiegen, bis keine weiteren __init__.py Dateien gefunden werden oder das Verzeichnis auf sys.path gefunden wird.

Dies kann potenziell ein teurer Prozess sein. Wenn die Paketstruktur tief ist, erfordert dies möglicherweise eine große Menge an Festplattenzugriffen, um zu ermitteln, wo das Paket in sys.path verankert ist, falls überhaupt. Allein die stat-Aufrufe können teuer sein, wenn das Dateisystem, auf dem sich das ausgeführte Skript befindet, etwas wie NFS ist.

Aufgrund dieser Probleme wird __name__ nur dann gesetzt, wenn das Befehlszeilenargument -m (eingeführt durch PEP 338) verwendet wird. Andernfalls tritt der Fallback-Mechanismus auf, bei dem __name__ auf "__main__" gesetzt wird. sys.main wird unabhängig davon, wie __name__ gesetzt ist, immer auf den korrekten Wert gesetzt.

Implementierung

Wenn die Option -m verwendet wird, wird sys.main auf das übergebene Argument gesetzt. sys.argv wird wie derzeit angepasst. Dann tritt die Entsprechung von __import__(self.main) auf. Dies unterscheidet sich von den aktuellen Semantiken, da das Modul runpy das Codeobjekt für die durch den Modulnamen angegebene Datei abruft, um explizit __name__ und andere Attribute festzulegen. Dies ist nicht mehr erforderlich, da der Import seine normale Operation in dieser Situation durchführen kann.

Wenn ein Dateiname angegeben wird, wird sys.main auf "__main__" gesetzt. Die angegebene Datei wird dann gelesen, ein Codeobjekt erstellt und mit __name__ auf "__main__" gesetzt und ausgeführt. Dies spiegelt die aktuellen Semantiken wider.

Migrationsplan

Damit Python 2.6 sowohl die aktuellen als auch die vorgeschlagenen Semantiken unterstützen kann, wird sys.main immer auf "__main__" gesetzt. Andernfalls ändert sich für Python 2.6 nichts. Das bedeutet leider, dass aus dieser Änderung in Python 2.6 kein Nutzen gezogen wird, maximiert aber die Kompatibilität für Code, der so gut wie möglich mit 2.6 und 3.0 funktioniert.

Um den Übergang zum neuen Idiom zu unterstützen, wird das 2to3-Tool [1] eine Regel erhalten, um das aktuelle Idiom if __name__ == '__main__': ... in das neue umzuwandeln. Dies hilft jedoch nicht bei Code, der __name__ außerhalb des Idioms überprüft.

Abgelehnte Ideen

__main__ Built-in

Ein Gegenvorschlag zur Einführung einer Built-in-Funktion namens __main__. Der Wert der Built-in-Funktion wäre der Name des ausgeführten Moduls (genau wie das vorgeschlagene sys.main). Dies würde zu einem neuen Idiom führen:

if __name__ == __main__:
    ...

Ein Nachteil ist, dass der syntaktische Unterschied subtil ist; das Weglassen von Anführungszeichen um "__main__". Einige glauben, dass bei bestehenden Python-Programmierern Fehler eingeführt werden, bei denen die Anführungszeichen versehentlich gesetzt werden. Man könnte jedoch argumentieren, dass der Fehler durch Tests schnell entdeckt würde, da es sich um einen sehr oberflächlichen Fehler handelt.

Während der Name der Built-in-Funktion natürlich anders sein könnte (z. B. main), ist der andere Nachteil, dass eine neue Built-in-Funktion eingeführt wird. Da eine einfache Lösung wie sys.main möglich ist, ohne eine weitere Built-in-Funktion zu Python hinzuzufügen, wurde dieser Vorschlag abgelehnt.

__main__ Modulattribut

Ein weiterer Vorschlag war, jedem Modul ein Attribut __main__ hinzuzufügen. Für das Modul, das als Hauptmodul ausgeführt wurde, hätte das Attribut einen wahren Wert, während alle anderen Module einen falschen Wert hätten. Dies hat die schöne Konsequenz, das Hauptmodul-Idiom zu vereinfachen:

if __main__:
    ...

Der Nachteil war die Einführung eines neuen Modulattributs. Es erforderte auch mehr Integration mit der Import-Maschinerie als die vorgeschlagene Lösung.

Verwenden Sie __file__ anstelle von __name__

Jeder der Vorschläge könnte geändert werden, um das Attribut __file__ in Modulen anstelle von __name__ zu verwenden, einschließlich der aktuellen Semantiken. Das Problem dabei ist, dass bei den vorgeschlagenen Lösungen die Module entweder kein definiertes __file__ Attribut haben oder denselben Wert wie andere Module.

Das Problem, das bei den aktuellen Semantiken auftritt, ist, dass man immer noch versuchen muss, den Dateipfad auf einen Modulnamen aufzulösen, damit der Import funktioniert.

Spezielle String-Unterklasse für __name__, die __eq__ überschreibt

Ein Vorschlag war, eine Unterklasse von str zu definieren, die die Methode __eq__ überschreibt, so dass sie sowohl mit "__main__" als auch mit dem tatsächlichen Namen des Moduls gleich verglichen wird. In allen anderen Belangen wäre die Unterklasse dieselbe wie str.

Dies wurde abgelehnt, da es wie ein zu großer Hack erschien.

Referenzen


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

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