PEP 547 – Ausführen von Erweiterungsmodulen über die Option -m
- Autor:
- Marcel Plch <gmarcel.plch at gmail.com>, Petr Viktorin <encukou at gmail.com>
- Status:
- Verschoben
- Typ:
- Standards Track
- Erstellt:
- 25-Mai-2017
- Python-Version:
- 3.7
- Post-History:
Zurückstellungsvermerk
Cython – der wichtigste Anwendungsfall für diesen PEP und der einzig explizite – ist noch nicht bereit für die Multi-Phase-Initialisierung. Es behält globalen Zustand in C-Level-Static-Variablen. Siehe Diskussion unter Cython Issue 1923.
Der PEP wird zurückgestellt, bis sich die Situation ändert.
Zusammenfassung
Dieser PEP schlägt eine Implementierung vor, die es ermöglicht, integrierte und Erweiterungsmodule im `__main__`-Namensraum unter Verwendung der PEP 489 Multi-Phase-Initialisierung auszuführen.
Damit kann ein Modul mit aktivierter Multi-Phase-Initialisierung mit folgendem Befehl ausgeführt werden
$ python3 -m _testmultiphase
This is a test module named __main__.
Motivation
Derzeit unterstützen Erweiterungsmodule nicht die gesamte Funktionalität von Python-Quellmodulen. Insbesondere ist es nicht möglich, Erweiterungsmodule als Skripte über die `-m`-Option von Python auszuführen.
Die technische Grundlage, um dies zu ermöglichen, wurde für PEP 489 geschaffen, und die Aktivierung der `-m`-Option ist in der Sektion „Mögliche zukünftige Erweiterungen“ dieses PEPs aufgeführt. Technisch gesehen sind die hier vorgeschlagenen zusätzlichen Änderungen relativ gering.
Begründung
Der fehlende Support für die `-m`-Option in Erweiterungsmodulen wurde traditionell durch die Bereitstellung eines Python-Wrappers umgangen. Zum Beispiel ist die Kommandozeilenschnittstelle des `_pickle`-Moduls im reinen Python `pickle`-Modul (zusammen mit einer reinen Python-Neuerstellung) untergebracht.
Dies funktioniert gut für Module der Standardbibliothek, da das Erstellen von Kommandozeilenschnittstellen mit der C-API umständlich ist. Andere Benutzer möchten jedoch möglicherweise direkt ausführbare Erweiterungsmodule erstellen.
Ein wichtiger Anwendungsfall ist Cython, eine Python-ähnliche Sprache, die zu C-Erweiterungsmodulen kompiliert wird. Cython ist eine (nahezu) Obermenge von Python, was bedeutet, dass das Kompilieren eines Python-Moduls mit Cython in der Regel die Funktionalität des Moduls nicht ändert und die schrittweise Hinzufügung Cython-spezifischer Funktionen ermöglicht. Dieser PEP wird es Cython-Erweiterungsmodulen ermöglichen, sich gleich zu verhalten wie ihre Python-Gegenstücke, wenn sie über die `-m`-Option ausgeführt werden. Cython-Entwickler halten das Feature für implementierungswürdig (siehe Cython Issue 1715).
Hintergrund
Die `-m`-Option von Python wird von der Funktion `runpy._run_module_as_main` gehandhabt.
Das durch `-m` angegebene Modul wird nicht normal importiert. Stattdessen wird es im Namensraum des `__main__`-Moduls ausgeführt, das ziemlich früh bei der Interpreter-Initialisierung erstellt wird.
Für Python-Quellmodule stellt die Ausführung im Namensraum eines anderen Moduls kein Problem dar: Der Code wird mit `locals` und `globals` ausgeführt, die auf das `__dict__` des bestehenden Moduls gesetzt sind. Dies ist bei Erweiterungsmodulen nicht der Fall, deren `PyInit_*`-Einstiegspunkt traditionell sowohl ein neues Modulobjekt (mittels `PyModule_Create`) erzeugte als auch dieses initialisierte.
Seit Python 3.5 können Erweiterungsmodule die PEP 489 Multi-Phase-Initialisierung verwenden. In diesem Szenario gibt der `PyInit_*`-Einstiegspunkt eine `PyModuleDef`-Struktur zurück: eine Beschreibung, wie das Modul erstellt und initialisiert werden soll. Die Erweiterung kann wählen, die Erstellung des Modulobjekts über den `Py_mod_create`-Callback anzupassen oder ein normales Modulobjekt zu verwenden, indem sie `Py_mod_create` nicht angibt. Ein weiterer Callback, `Py_mod_exec`, wird dann aufgerufen, um das Modulobjekt zu initialisieren, z.B. durch Befüllen mit Methoden und Klassen.
Vorschlag
Die Multi-Phase-Initialisierung macht es möglich, ein Erweiterungsmodul im Namensraum eines anderen Moduls auszuführen: Wenn kein `Py_mod_create`-Callback angegeben ist, kann das `__main__`-Modul an den `Py_mod_exec`-Callback übergeben werden, um initialisiert zu werden, als ob `__main__` ein frisch erstelltes Modulobjekt wäre.
Eine Komplikation in diesem Schema ist der Modulzustand auf C-Ebene. Jedes Modul hat einen `md_state`-Zeiger, der auf einen Speicherbereich zeigt, der bei der Erstellung eines Erweiterungsmoduls zugewiesen wird. Die `PyModuleDef` gibt an, wie viel Speicher zugewiesen werden soll.
Die Implementierung muss darauf achten, dass der `md_state`-Speicher höchstens einmal zugewiesen wird. Außerdem sollte der `Py_mod_exec`-Callback nur einmal pro Modul aufgerufen werden. Die Auswirkungen von mehrfach initialisierten Modulen sind zu subtil, als dass man von Erweiterungsautoren erwarten könnte, dass sie darüber nachdenken. Der `md_state`-Zeiger selbst dient als Schutz: Die Zuweisung des Speichers und der Aufruf von `Py_mod_exec` erfolgen immer zusammen, und die Initialisierung eines Erweiterungsmoduls schlägt fehl, wenn `md_state` bereits nicht-NULL ist.
Da das `__main__`-Modul nicht als Erweiterungsmodul erstellt wird, ist sein `md_state` normalerweise `NULL`. Vor der Initialisierung eines Erweiterungsmoduls im Kontext von `__main__` wird sein Modulzustand gemäß der `PyModuleDef` dieses Moduls zugewiesen.
Während PEP 489 darauf ausgelegt war, diese Änderungen allgemein zu ermöglichen, ist es notwendig, die Schritte Modulerkennung, -erstellung und -initialisierung für Erweiterungsmodule zu entkoppeln, so dass ein anderes Modul anstelle eines neu initialisierten verwendet werden kann, und die Funktionalität muss zu `runpy` und `importlib` hinzugefügt werden.
Spezifikation
Eine neue optionale Methode für importlib-Loader wird hinzugefügt. Diese Methode wird `exec_in_module` genannt und nimmt zwei Positionsargumente entgegen: den Modulspezifikator und ein bereits vorhandenes Modul. Alle importbezogenen Attribute, wie z.B. `__spec__` oder `__name__`, die bereits im Modul gesetzt sind, werden ignoriert.
Die Funktion `runpy._run_module_as_main` wird nach dieser neuen Loader-Methode suchen. Wenn sie vorhanden ist, wird `runpy` sie ausführen, anstatt zu versuchen, den Python-Code des Moduls zu laden und auszuführen. Andernfalls verhält sich `runpy` wie bisher.
Änderungen an ExtensionFileLoader
Der `ExtensionFileLoader` von importlib erhält eine Implementierung von `exec_in_module`, die eine neue Funktion, `_imp.exec_in_module`, aufruft.
`_imp.exec_in_module` wird die bestehenden Mechanismen nutzen, um die `PyInit_*`-Funktion eines Erweiterungsmoduls zu finden und aufzurufen.
Die `PyInit_*`-Funktion kann entweder ein vollständig initialisiertes Modul (Single-Phase-Initialisierung) oder eine `PyModuleDef` (für PEP 489 Multi-Phase-Initialisierung) zurückgeben.
Im Fall der Single-Phase-Initialisierung wird `_imp.exec_in_module` einen `ImportError` auslösen.
Im Fall der Multi-Phase-Initialisierung werden die `PyModuleDef` und das zu initialisierende Modul an eine neue Funktion, `PyModule_ExecInModule`, übergeben.
Diese Funktion löst einen `ImportError` aus, wenn die `PyModuleDef` einen `Py_mod_create`-Slot angibt oder wenn das Modul bereits initialisiert wurde (d.h. sein `md_state`-Zeiger ist nicht `NULL`). Andernfalls initialisiert die Funktion das Modul gemäß der `PyModuleDef`.
Abwärtskompatibilität
Dieser PEP wahrt die Abwärtskompatibilität. Er fügt nur neue Funktionen und eine neue Loader-Methode hinzu, die für einen Loader hinzugefügt wird, der zuvor das Ausführen von Modulen als `__main__` nicht unterstützte.
Referenzimplementierung
Die Referenzimplementierung dieses PEPs ist auf GitHub verfügbar.
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0547.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT