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

Python Enhancement Proposals

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:


Inhaltsverzeichnis

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.


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

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