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

Python Enhancement Proposals

PEP 489 – Multi-Phasen-Initialisierung von Erweiterungsmodulen

Autor:
Petr Viktorin <encukou at gmail.com>, Stefan Behnel <stefan_ml at behnel.de>, Alyssa Coghlan <ncoghlan at gmail.com>
BDFL-Delegate:
Eric Snow <ericsnowcurrently at gmail.com>
Discussions-To:
Import-SIG Liste
Status:
Final
Typ:
Standards Track
Erstellt:
11-Aug-2013
Python-Version:
3.5
Post-History:
23-Aug-2013, 20-Feb-2015, 16-Apr-2015, 07-Mai-2015, 18-Mai-2015
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Wichtig

Dieses PEP ist ein historisches Dokument. Die aktuelle, kanonische Dokumentation finden Sie jetzt unter Initialisierung von C-Modulen. Für Python 3.14+ siehe Definition von Erweiterungsmodulen und Moduldefinitionen.

×

Siehe PEP 1, um Änderungen vorzuschlagen.

Zusammenfassung

Dieses PEP schlägt eine Neugestaltung der Art und Weise vor, wie integrierte und Erweiterungsmodule mit der Import-Maschinerie interagieren. Dies wurde zuletzt für Python 3.0 in PEP 3121 überarbeitet, löste aber damals nicht alle Probleme. Ziel ist es, importbezogene Probleme zu lösen, indem Erweiterungsmodule näher an das Verhalten von Python-Modulen gebracht werden; insbesondere soll in den Modul-Spec-basierten Ladevorgang eingehakt werden, der in PEP 451 eingeführt wurde.

Dieser Vorschlag ist inspiriert von PyType_Spec aus PEP 384, um Erweiterungsautoren zu ermöglichen, nur die benötigten Funktionen zu definieren und zukünftige Ergänzungen zu Erweiterungsmoduldeklarationen zu ermöglichen.

Erweiterungsmodule werden in einem zweistufigen Prozess erstellt, der besser in die Modul-Spec-Architektur passt und Parallelen zu __new__ und __init__ von Klassen aufweist.

Erweiterungsmodule können sicher beliebige C-Level-Modulzustände im Modul speichern, die von der normalen Garbage Collection abgedeckt werden und das Neuladen sowie Sub-Interpreter unterstützen. Erweiterungsautoren werden ermutigt, diese Aspekte bei der Verwendung der neuen API zu berücksichtigen.

Der Vorschlag erlaubt auch Erweiterungsmodule mit nicht-ASCII-Namen.

Nicht alle Probleme, die in PEP 3121 angegangen wurden, sind in diesem Vorschlag gelöst. Insbesondere Probleme mit der Laufzeit-Modulsuche (PyState_FindModule) werden einem zukünftigen PEP überlassen.

Motivation

Python-Module und Erweiterungsmodule werden nicht auf dieselbe Weise eingerichtet. Bei Python-Modulen wird zuerst das Modulobjekt erstellt und eingerichtet, dann wird der Modulcode ausgeführt (PEP 302). Ein Modul-Spec-Objekt (PEP 451) wird verwendet, um Informationen über das Modul zu speichern und an die relevanten Hooks übergeben.

Bei Erweiterungen (d. h. dynamischen Bibliotheken) und integrierten Modulen wird die Modul-Init-Funktion sofort ausgeführt und übernimmt sowohl die Erstellung als auch die Initialisierung. Die Initialisierungsfunktion erhält nicht die Modul-Spec oder Informationen daraus, wie z. B. __file__ oder den voll qualifizierten Namen. Dies behindert relative Imports und das Laden von Ressourcen.

In Py3 werden Module auch nicht zu sys.modules hinzugefügt, was bedeutet, dass ein (möglicherweise transitiver) Neuimport des Moduls es tatsächlich versucht und somit in eine Endlosschleife gerät, wenn es die Modul-Init-Funktion erneut ausführt. Ohne Zugriff auf den voll qualifizierten Modulnamen ist es auch nicht trivial, das Modul korrekt zu sys.modules hinzuzufügen. Dies ist insbesondere ein Problem für Cython-generierte Module, bei denen es nicht unüblich ist, dass der Modul-Init-Code die gleiche Komplexität aufweist wie bei jedem "regulären" Python-Modul. Auch der Mangel an Informationen über __file__ und __name__ behindert die Kompilierung von " __init__.py"-Modulen, d. h. Paketen, insbesondere bei Verwendung relativer Imports zur Modul-Initialisierungszeit.

Darüber hinaus haben die meisten aktuell bestehenden Erweiterungsmodule Probleme mit der Unterstützung von Sub-Interpretern und/oder Interpreter-Neuladen, und obwohl dies mit der aktuellen Infrastruktur möglich ist, ist es weder einfach noch effizient. Die Behebung dieser Probleme war das Ziel von PEP 3121, aber viele Erweiterungen, einschließlich einiger in der Standardbibliothek, wählten den Ansatz des geringsten Aufwands für die Portierung auf Python 3 und ließen diese Probleme ungelöst. Dieses PEP behält die Rückwärtskompatibilität bei, was den Druck verringern und Erweiterungsautoren ausreichend Zeit geben sollte, diese Probleme bei der Portierung zu berücksichtigen.

Der aktuelle Prozess

Derzeit exportieren Erweiterungs- und integrierte Module eine Initialisierungsfunktion namens "PyInit_modulename", benannt nach dem Dateinamen der dynamischen Bibliothek. Diese Funktion wird von der Import-Maschinerie ausgeführt und muss ein vollständig initialisiertes Modulobjekt zurückgeben. Die Funktion erhält keine Argumente, so dass sie nichts über ihren Importkontext weiß.

Während ihrer Ausführung erstellt die Modul-Init-Funktion ein Modulobjekt basierend auf einem PyModuleDef-Objekt. Dann initialisiert sie es weiter, indem sie Attribute zum Modul-Dictionary hinzufügt, Typen erstellt usw.

Im Hintergrund behält der Lader der dynamischen Bibliothek eine Notiz über den voll qualifizierten Modulnamen des zuletzt geladenen Moduls, und wenn ein Modul mit übereinstimmendem Namen erstellt wird, wird diese globale Variable verwendet, um den voll qualifizierten Namen des Modulobjekts zu bestimmen. Dies ist nicht ganz sicher, da es davon abhängt, dass die Modul-Init-Funktion zuerst ihr eigenes Modulobjekt erstellt, aber diese Annahme hält in der Praxis normalerweise stand.

Der Vorschlag

Die Initialisierungsfunktion (PyInit_modulename) darf einen Zeiger auf ein PyModuleDef-Objekt zurückgeben. Die Import-Maschinerie ist dafür verantwortlich, das Modulobjekt zu erstellen und Hooks, die in der PyModuleDef angegeben sind, in den relevanten Phasen der Initialisierung (wie unten beschrieben) aufzurufen.

Diese Multi-Phasen-Initialisierung ist eine zusätzliche Möglichkeit. Die Einzelphasen-Initialisierung, die aktuelle Praxis, ein vollständig initialisiertes Modulobjekt zurückzugeben, wird weiterhin akzeptiert, so dass bestehender Code unverändert funktioniert, einschließlich binärer Kompatibilität.

Die Struktur PyModuleDef wird geändert, um eine Liste von Slots zu enthalten, ähnlich wie PyType_Spec aus PEP 384 für Typen. Um die binäre Kompatibilität zu erhalten und die Einführung einer neuen Struktur zu vermeiden (was zusätzliche unterstützende Funktionen und pro Modul Speicherplatz einführen würde), wird der derzeit ungenutzte m_reload-Zeiger von PyModuleDef geändert, um die Slots zu halten. Die Strukturen sind definiert als

typedef struct {
    int slot;
    void *value;
} PyModuleDef_Slot;

typedef struct PyModuleDef {
    PyModuleDef_Base m_base;
    const char* m_name;
    const char* m_doc;
    Py_ssize_t m_size;
    PyMethodDef *m_methods;
    PyModuleDef_Slot *m_slots;  /* changed from `inquiry m_reload;` */
    traverseproc m_traverse;
    inquiry m_clear;
    freefunc m_free;
} PyModuleDef;

Das m_slots-Mitglied muss entweder NULL sein oder auf ein Array von PyModuleDef_Slot-Strukturen zeigen, die durch einen Slot mit der ID 0 abgeschlossen werden (d. h. {0, NULL}).

Um einen Slot anzugeben, muss eine eindeutige Slot-ID bereitgestellt werden. Neue Python-Versionen können neue Slot-IDs einführen, aber Slot-IDs werden niemals wiederverwendet. Slots können veraltet sein, aber werden während der gesamten Python 3.x-Version unterstützt.

Der Wertzeiger eines Slots darf nicht NULL sein, es sei denn, dies wird in der Dokumentation des Slots anderweitig angegeben.

Die folgenden Slots sind derzeit verfügbar und werden später beschrieben

  • Py_mod_create
  • Py_mod_exec

Unbekannte Slot-IDs führen dazu, dass der Import mit SystemError fehlschlägt.

Bei Verwendung der Multi-Phasen-Initialisierung wird das m_name-Feld von PyModuleDef während des Imports nicht verwendet; der Modulname wird aus der Modul-Spec übernommen.

Bevor das PyModuleDef-Objekt von PyInit zurückgegeben wird, muss es mit der neu hinzugefügten Funktion PyModuleDef_Init initialisiert werden. Dies setzt den Objekttyp (der bei bestimmten Compilern nicht statisch gesetzt werden kann), die Referenzanzahl und interne Buchhaltungsdaten (m_index). Zum Beispiel würde ein Erweiterungsmodul "example" exportiert als

static PyModuleDef example_def = {...}

PyMODINIT_FUNC
PyInit_example(void)
{
    return PyModuleDef_Init(&example_def);
}

Das PyModuleDef-Objekt muss für die gesamte Lebensdauer des daraus erstellten Moduls verfügbar sein – üblicherweise wird es statisch deklariert.

Pseudo-Code-Übersicht

Hier ist eine Übersicht, wie die modifizierten Importer funktionieren werden. Details wie Protokollierung oder die Behandlung von Fehlern und ungültigen Zuständen werden weggelassen, und C-Code wird mit einer knappen Python-ähnlichen Syntax dargestellt.

Das Framework, das die Importer aufruft, wird in PEP 451 erläutert.

importlib/_bootstrap.py:

class BuiltinImporter:
    def create_module(self, spec):
        module = _imp.create_builtin(spec)

    def exec_module(self, module):
        _imp.exec_dynamic(module)

    def load_module(self, name):
        # use a backwards compatibility shim
        _load_module_shim(self, name)

importlib/_bootstrap_external.py:

class ExtensionFileLoader:
    def create_module(self, spec):
        module = _imp.create_dynamic(spec)

    def exec_module(self, module):
        _imp.exec_dynamic(module)

    def load_module(self, name):
        # use a backwards compatibility shim
        _load_module_shim(self, name)

Python/import.c (das Modul _imp)

def create_dynamic(spec):
    name = spec.name
    path = spec.origin

    # Find an already loaded module that used single-phase init.
    # For multi-phase initialization, mod is NULL, so a new module
    # is always created.
    mod = _PyImport_FindExtensionObject(name, name)
    if mod:
        return mod

    return _PyImport_LoadDynamicModuleWithSpec(spec)

def exec_dynamic(module):
    if not isinstance(module, types.ModuleType):
        # non-modules are skipped -- PyModule_GetDef fails on them
        return

    def = PyModule_GetDef(module)
    state = PyModule_GetState(module)
    if state is NULL:
        PyModule_ExecDef(module, def)

def create_builtin(spec):
    name = spec.name

    # Find an already loaded module that used single-phase init.
    # For multi-phase initialization, mod is NULL, so a new module
    # is always created.
    mod = _PyImport_FindExtensionObject(name, name)
    if mod:
        return mod

    for initname, initfunc in PyImport_Inittab:
        if name == initname:
            m = initfunc()
            if isinstance(m, PyModuleDef):
                def = m
                return PyModule_FromDefAndSpec(def, spec)
            else:
                # fall back to single-phase initialization
                module = m
                _PyImport_FixupExtensionObject(module, name, name)
                return module

Python/importdl.c:

def _PyImport_LoadDynamicModuleWithSpec(spec):
    path = spec.origin
    package, dot, name = spec.name.rpartition('.')

    # see the "Non-ASCII module names" section for export_hook_name
    hook_name = export_hook_name(name)

    # call platform-specific function for loading exported function
    # from shared library
    exportfunc = _find_shared_funcptr(hook_name, path)

    m = exportfunc()
    if isinstance(m, PyModuleDef):
        def = m
        return PyModule_FromDefAndSpec(def, spec)

    module = m

    # fall back to single-phase initialization
    ....

Objects/moduleobject.c:

def PyModule_FromDefAndSpec(def, spec):
    name = spec.name
    create = None
    for slot, value in def.m_slots:
        if slot == Py_mod_create:
            create = value
    if create:
        m = create(spec, def)
    else:
        m = PyModule_New(name)

    if isinstance(m, types.ModuleType):
        m.md_state = None
        m.md_def = def

    if def.m_methods:
        PyModule_AddFunctions(m, def.m_methods)
    if def.m_doc:
        PyModule_SetDocString(m, def.m_doc)

def PyModule_ExecDef(module, def):
    if isinstance(module, types.module_type):
        if module.md_state is NULL:
            # allocate a block of zeroed-out memory
            module.md_state = _alloc(module.md_size)

    if def.m_slots is NULL:
        return

    for slot, value in def.m_slots:
        if slot == Py_mod_exec:
            value(module)

Modulerstellungsphase

Die Erstellung des Modulobjekts – also die Implementierung von ExecutionLoader.create_module – wird durch den Py_mod_create Slot gesteuert.

Der Py_mod_create Slot

Der Py_mod_create Slot wird verwendet, um benutzerdefinierte Modul-Unterklassen zu unterstützen. Der Wertzeiger muss auf eine Funktion mit folgender Signatur zeigen

PyObject* (*PyModuleCreateFunction)(PyObject *spec, PyModuleDef *def)

Die Funktion erhält eine Modul-Spec-Instanz, wie in PEP 451 definiert, und die PyModuleDef-Struktur. Sie sollte ein neues Modulobjekt zurückgeben oder einen Fehler setzen und NULL zurückgeben.

Diese Funktion ist nicht dafür verantwortlich, Import-bezogene Attribute, die in PEP 451 (wie __name__ oder __loader__) spezifiziert sind, auf dem neuen Modul zu setzen.

Es gibt keine Anforderung, dass das zurückgegebene Objekt eine Instanz von types.ModuleType ist. Jeder Typ kann verwendet werden, solange er das Setzen und Abrufen von Attributen unterstützt, einschließlich mindestens der Import-bezogenen Attribute. Allerdings unterstützen nur ModuleType-Instanzen Modul-spezifische Funktionalitäten wie Pro-Modul-Status und die Verarbeitung von Ausführungs-Slots. Wenn etwas anderes als eine Unterklasse von ModuleType zurückgegeben wird, dürfen keine Ausführungs-Slots definiert werden; wenn doch, wird ein SystemError ausgelöst.

Beachten Sie, dass zum Zeitpunkt des Aufrufs dieser Funktion der Eintrag des Moduls in sys.modules noch nicht gefüllt ist. Der Versuch, dasselbe Modul erneut zu importieren (möglicherweise transitiv), kann zu einer Endlosschleife führen. Erweiterungsautoren wird geraten, Py_mod_create minimal zu halten und insbesondere keinen Benutzer-Code daraus aufzurufen.

Mehrere Py_mod_create Slots dürfen nicht angegeben werden. Wenn doch, schlägt der Import mit SystemError fehl.

Wenn Py_mod_create nicht angegeben ist, erstellt die Import-Maschinerie ein normales Modulobjekt mit PyModule_New. Der Name wird aus spec genommen.

Schritte nach der Erstellung

Wenn die Funktion Py_mod_create eine Instanz von types.ModuleType oder eine Unterklasse zurückgibt (oder wenn kein Py_mod_create Slot vorhanden ist), ordnet die Import-Maschinerie die PyModuleDef dem Modul zu. Dies macht die PyModuleDef auch für die Ausführungsphase, die Funktion PyModule_GetDef und die Garbage-Collection-Routinen (traverse, clear, free) zugänglich.

Wenn die Funktion Py_mod_create keine Modul-Unterklasse zurückgibt, dann muss m_size 0 sein und m_traverse, m_clear und m_free müssen alle NULL sein. Andernfalls wird SystemError ausgelöst.

Zusätzlich werden anfängliche Attribute, die in der PyModuleDef angegeben sind, auf dem Modulobjekt gesetzt, unabhängig von seinem Typ

  • Der Docstring wird aus m_doc gesetzt, falls er nicht NULL ist.
  • Die Funktionen des Moduls werden aus m_methods initialisiert, falls vorhanden.

Modulexekutionsphase

Modulausführung – d. h. die Implementierung von ExecutionLoader.exec_module – wird durch "Ausführungs-Slots" gesteuert. Dieses PEP fügt nur einen hinzu, Py_mod_exec, aber andere können in Zukunft hinzugefügt werden.

Die Ausführungsphase wird auf der PyModuleDef, die dem Modulobjekt zugeordnet ist, durchgeführt. Für Objekte, die keine Unterklasse von PyModule_Type sind (für die PyModule_GetDef fehlschlagen würde), wird die Ausführungsphase übersprungen.

Ausführungs-Slots können mehrmals angegeben werden und werden in der Reihenfolge verarbeitet, in der sie im Slot-Array erscheinen. Bei Verwendung der Standard-Import-Maschinerie werden sie nach dem Setzen von Import-bezogenen Attributen, die in PEP 451 angegeben sind (wie __name__ oder __loader__), und nachdem das Modul zu sys.modules hinzugefügt wurde, verarbeitet.

Schritte vor der Ausführung

Vor der Verarbeitung der Ausführungs-Slots wird pro Modul Speicher für den Modulstatus zugewiesen. Von diesem Zeitpunkt an ist der pro Modul Status über PyModule_GetState zugänglich.

Der Py_mod_exec Slot

Der Eintrag in diesem Slot muss auf eine Funktion mit folgender Signatur zeigen

int (*PyModuleExecFunction)(PyObject* module)

Sie wird aufgerufen, um ein Modul zu initialisieren. Dies beinhaltet normalerweise das Setzen der anfänglichen Attribute des Moduls. Das "module"-Argument erhält das zu initialisierende Modulobjekt.

Die Funktion muss bei Erfolg 0 zurückgeben oder bei einem Fehler eine Exception setzen und -1 zurückgeben.

Wenn PyModuleExec den Eintrag des Moduls in sys.modules ersetzt, wird das neue Objekt verwendet und von der Importlib-Maschinerie zurückgegeben, nachdem alle Ausführungs-Slots verarbeitet wurden. Dies ist ein Feature der Import-Maschinerie selbst. Die Slots selbst werden alle mit dem Modulobjekt verarbeitet, das aus der Erstellungsphase zurückgegeben wurde; sys.modules wird während der Ausführungsphase nicht konsultiert. (Beachten Sie, dass für Erweiterungsmodule die Implementierung von Py_mod_create normalerweise eine bessere Lösung für die Verwendung benutzerdefinierter Modulobjekte ist.)

Legacy Init

Die abwärtskompatible Einzelphasen-Initialisierung wird weiterhin unterstützt. In diesem Schema gibt die PyInit-Funktion ein vollständig initialisiertes Modul zurück, anstatt ein PyModuleDef-Objekt. In diesem Fall implementiert der PyInit-Hook die Erstellungsphase, und die Ausführungsphase ist eine No-Op.

Module, die unverändert auf älteren Python-Versionen funktionieren müssen, sollten bei der Einzelphasen-Initialisierung bleiben, da die damit verbundenen Vorteile nicht rückportiert werden können. Hier ist ein Beispiel für ein Modul, das die Multi-Phasen-Initialisierung unterstützt und auf eine ältere Version von CPython zurückfällt, wenn es für diese kompiliert wird. Es wird hauptsächlich als Illustration der benötigten Änderungen zur Ermöglichung der Multi-Phasen-Init enthalten.

#include <Python.h>

static int spam_exec(PyObject *module) {
    PyModule_AddStringConstant(module, "food", "spam");
    return 0;
}

#ifdef Py_mod_exec
static PyModuleDef_Slot spam_slots[] = {
    {Py_mod_exec, spam_exec},
    {0, NULL}
};
#endif

static PyModuleDef spam_def = {
    PyModuleDef_HEAD_INIT,                      /* m_base */
    "spam",                                     /* m_name */
    PyDoc_STR("Utilities for cooking spam"),    /* m_doc */
    0,                                          /* m_size */
    NULL,                                       /* m_methods */
#ifdef Py_mod_exec
    spam_slots,                                 /* m_slots */
#else
    NULL,
#endif
    NULL,                                       /* m_traverse */
    NULL,                                       /* m_clear */
    NULL,                                       /* m_free */
};

PyMODINIT_FUNC
PyInit_spam(void) {
#ifdef Py_mod_exec
    return PyModuleDef_Init(&spam_def);
#else
    PyObject *module;
    module = PyModule_Create(&spam_def);
    if (module == NULL) return NULL;
    if (spam_exec(module) != 0) {
        Py_DECREF(module);
        return NULL;
    }
    return module;
#endif
}

Eingebaute Module

Jedes Erweiterungsmodul kann als integriertes Modul verwendet werden, indem es in die ausführbare Datei gelinkt und in die inittab aufgenommen wird (entweder zur Laufzeit mit PyImport_AppendInittab oder zur Konfigurationszeit mit Werkzeugen wie freeze).

Um diese Möglichkeit zu erhalten, werden alle Änderungen an der Ladung von Erweiterungsmodulen, die in diesem PEP eingeführt wurden, auch für integrierte Module gelten. Die einzige Ausnahme sind nicht-ASCII-Modulnamen, die unten erklärt werden.

Sub-Interpreter und Interpreter-Neuladen

Erweiterungen, die das neue Initialisierungsschema verwenden, sollten Sub-Interpreter und mehrere Py_Initialize/Py_Finalize-Zyklen korrekt unterstützen und die in der Python-Dokumentation erwähnten Probleme vermeiden [6]. Der Mechanismus ist darauf ausgelegt, dies zu erleichtern, aber vom Erweiterungsautor ist dennoch Sorgfalt geboten. Keine benutzerdefinierten Funktionen, Methoden oder Instanzen dürfen in verschiedene Interpreter gelangen. Um dies zu erreichen, sollte der gesamte Modulstatus entweder im Modul-Dictionary oder im Speicher des Modulobjekts, erreichbar über PyModule_GetState, gespeichert werden. Eine einfache Faustregel lautet: Definieren Sie keine statischen Daten, außer integrierte Typen ohne veränderliche oder vom Benutzer einstellbare Klassenattribute.

Funktionen, die mit der Multi-Phasen-Initialisierung inkompatibel sind

Die Funktion PyModule_Create schlägt fehl, wenn sie auf einer PyModuleDef-Struktur mit einem nicht-NULL m_slots-Zeiger verwendet wird. Die Funktion hat keinen Zugriff auf das Modul-Spec-Objekt, das für die Multi-Phasen-Initialisierung erforderlich ist.

Die Funktion PyState_FindModule gibt NULL zurück, und PyState_AddModule und PyState_RemoveModule schlagen ebenfalls fehl bei Modulen mit nicht-NULL m_slots. Die PyState-Registrierung ist deaktiviert, da mehrere Modulobjekte aus derselben PyModuleDef erstellt werden können.

Modulstatus und Callback auf C-Ebene

Aufgrund der Nichtverfügbarkeit von PyState_FindModule muss jede Funktion, die auf Modulstatus zugreifen muss (einschließlich Funktionen, Klassen oder Ausnahmen, die auf Modulebene definiert sind), eine Referenz auf das Modulobjekt (oder das benötigte Objekt) erhalten, entweder direkt oder indirekt. Dies ist derzeit in zwei Situationen schwierig

  • Methoden von Klassen, die eine Referenz auf die Klasse erhalten, aber nicht auf das Modul der Klasse
  • Bibliotheken mit Callbacks auf C-Ebene, es sei denn, die Callbacks können benutzerdefinierte Daten empfangen, die bei der Registrierung des Callbacks gesetzt wurden

Die Behebung dieser Fälle liegt außerhalb des Umfangs dieses PEP, ist aber notwendig, damit der neue Mechanismus für alle Module nützlich ist. Geeignete Lösungen wurden in der Import-Sig-Mailingliste diskutiert [5].

Als Faustregel gilt: Module, die auf PyState_FindModule angewiesen sind, sind derzeit keine guten Kandidaten für die Portierung auf den neuen Mechanismus.

Neue Funktionen

Eine neue Funktion und Makro, die die Modulerstellungsphase implementieren, werden hinzugefügt. Diese sind ähnlich zu PyModule_Create und PyModule_Create2, außer dass sie ein zusätzliches Modul-Spec-Argument erhalten und Moduldefinitionen mit nicht-NULL-Slots behandeln.

PyObject * PyModule_FromDefAndSpec(PyModuleDef *def, PyObject *spec)
PyObject * PyModule_FromDefAndSpec2(PyModuleDef *def, PyObject *spec,
                                    int module_api_version)

Eine neue Funktion, die die Modulausführungsphase implementiert, wird hinzugefügt. Diese weist pro Modul Speicher zu (falls noch nicht geschehen) und verarbeitet immer die Ausführungs-Slots. Die Import-Maschinerie ruft diese Methode auf, wenn ein Modul ausgeführt wird, es sei denn, das Modul wird neu geladen.

PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def)

Eine weitere Funktion wird eingeführt, um ein PyModuleDef-Objekt zu initialisieren. Diese idempotente Funktion füllt den Typ, die Referenzanzahl und den Modulindex aus. Sie gibt ihr Argument, gecastet auf PyObject*, zurück, so dass es direkt von einer PyInit-Funktion zurückgegeben werden kann.

PyObject * PyModuleDef_Init(PyModuleDef *);

Zusätzlich werden zwei Helfer zum Setzen des Docstrings und der Methoden auf einem Modul hinzugefügt.

int PyModule_SetDocString(PyObject *, const char *)
int PyModule_AddFunctions(PyObject *, PyMethodDef *)

Name des Export-Hooks

Da portable C-Bezeichner auf ASCII beschränkt sind, müssen Modulnamen kodiert werden, um den PyInit-Hook-Namen zu bilden.

Für ASCII-Modulnamen heißt der Import-Hook PyInit_<modulename>, wobei <modulename> der Name des Moduls ist.

Für Modulnamen, die Nicht-ASCII-Zeichen enthalten, heißt der Import-Hook PyInitU_<encodedname>, wobei der Name mit CPythons "Punycode"-Kodierung kodiert ist (Punycode mit einem Kleinbuchstaben-Suffix), wobei Bindestriche ("-") durch Unterstriche ("_") ersetzt werden.

In Python

def export_hook_name(name):
    try:
        suffix = b'_' + name.encode('ascii')
    except UnicodeEncodeError:
        suffix = b'U_' + name.encode('punycode').replace(b'-', b'_')
    return b'PyInit' + suffix

Beispiele

Modulname Init-Hook-Name
spam PyInit_spam
lančmít PyInitU_lanmt_2sa6t
スパム PyInitU_zck5b2b

Für Module mit Nicht-ASCII-Namen wird die Einzelphasen-Initialisierung nicht unterstützt.

In der ersten Implementierung dieses PEP werden integrierte Module mit Nicht-ASCII-Namen nicht unterstützt.

Modul-Neuladen

Das Neuladen eines Erweiterungsmoduls mit importlib.reload() hat weiterhin keine Auswirkung, außer dem erneuten Setzen von Import-bezogenen Attributen.

Aufgrund von Einschränkungen bei der Ladung dynamischer Bibliotheken (sowohl dlopen unter POSIX als auch LoadModuleEx unter Windows) ist es im Allgemeinen nicht möglich, eine geänderte Bibliothek zu laden, nachdem sie auf der Festplatte geändert wurde.

Anwendungsfälle für das Neuladen, die über das Ausprobieren einer neuen Modulversion hinausgehen, sind zu selten, als dass alle Modulautoren das Neuladen berücksichtigen müssten. Wenn eine neuladeähnliche Funktionalität benötigt wird, können Autoren eine dedizierte Funktion dafür exportieren.

Mehrere Module in einer Bibliothek

Um mehrere Python-Module in einer einzigen dynamischen Bibliothek zu unterstützen, kann die Bibliothek neben der, die dem Dateinamen der Bibliothek entspricht, zusätzliche PyInit* Symbole exportieren.

Beachten Sie, dass dieser Mechanismus derzeit nur zum *Laden* zusätzlicher Module verwendet werden kann, nicht zum *Finden* derselben. (Dies ist eine Einschränkung des Lader-Mechanismus, den dieses PEP nicht zu ändern versucht.) Um den Mangel an einem geeigneten Finder zu umgehen, kann Code wie der folgende verwendet werden

import importlib.machinery
import importlib.util
loader = importlib.machinery.ExtensionFileLoader(name, path)
spec = importlib.util.spec_from_loader(name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module

Auf Plattformen, die symbolische Links unterstützen, können diese verwendet werden, um eine Bibliothek unter mehreren Namen zu installieren und alle exportierten Module der normalen Import-Maschinerie zur Verfügung zu stellen.

Tests und erste Implementierungen

Für Tests wird ein neues integriertes Modul _testmultiphase erstellt. Die Bibliothek wird mehrere zusätzliche Module unter Verwendung des Mechanismus "Mehrere Module in einer Bibliothek" exportieren.

Das Modul _testcapi bleibt unverändert und wird auf unbestimmte Zeit die Einzelphasen-Initialisierung verwenden (oder bis sie nicht mehr unterstützt wird).

Die Module array und xx* werden als Teil der ersten Implementierung auf die Multi-Phasen-Initialisierung umgestellt.

Zusammenfassung der API-Änderungen und Ergänzungen

Neue Funktionen

  • PyModule_FromDefAndSpec (Makro)
  • PyModule_FromDefAndSpec2
  • PyModule_ExecDef
  • PyModule_SetDocString
  • PyModule_AddFunctions
  • PyModuleDef_Init

Neue Makros

  • Py_mod_create
  • Py_mod_exec

Neue Typen

  • PyModuleDef_Type wird freigegeben

Neue Strukturen

  • PyModuleDef_Slot

Weitere Änderungen

PyModuleDef.m_reload wird zu PyModuleDef.m_slots geändert.

BuiltinImporter und ExtensionFileLoader implementieren nun create_module und exec_module.

Das interne Modul _imp wird abwärtsinkompatible Änderungen erfahren: create_builtin, create_dynamic und exec_dynamic werden hinzugefügt; init_builtin, load_dynamic werden entfernt.

Die undokumentierten Funktionen imp.load_dynamic und imp.init_builtin werden durch abwärtskompatible Shim-Funktionen ersetzt.

Abwärtskompatibilität

Bestehende Module bleiben quellcode- und binärkompatibel mit neuen Python-Versionen. Module, die die Multi-Phasen-Initialisierung verwenden, sind nicht mit Python-Versionen kompatibel, die dieses PEP nicht implementieren.

Die Funktionen init_builtin und load_dynamic werden aus dem Modul _imp entfernt (nicht aber aus dem Modul imp).

Alle geänderten Lader (BuiltinImporter und ExtensionFileLoader) bleiben abwärtskompatibel; die Methode load_module wird durch eine Shim-Funktion ersetzt.

Interne Funktionen von Python/import.c und Python/importdl.c werden entfernt. (Insbesondere sind dies _PyImport_GetDynLoadFunc, _PyImport_GetDynLoadWindows und _PyImport_LoadDynamicModule.)

Mögliche zukünftige Erweiterungen

Der Slot-Mechanismus, inspiriert von PyType_Slot aus PEP 384, ermöglicht spätere Erweiterungen.

Einige Erweiterungsmodule exportieren viele Konstanten; z. B. hat _ssl eine lange Liste von Aufrufen im Format

PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN",
                        PY_SSL_ERROR_ZERO_RETURN);

Die Konvertierung dies in eine deklarative Liste, ähnlich wie bei PyMethodDef, würde Boilerplate reduzieren und kostenlose Fehlerüberprüfung bieten, die oft fehlt.

Zeichenkettenkonstanten und Typen können ähnlich behandelt werden. (Beachten Sie, dass Nicht-Standard-Basen für Typen nicht portabel statisch angegeben werden können; dieser Fall würde eine Py_mod_exec-Funktion erfordern, die läuft, bevor die Slots hinzugefügt werden. Die kostenlose Fehlerüberprüfung wäre jedoch immer noch von Vorteil.)

Eine weitere Möglichkeit ist die Bereitstellung einer " main"-Funktion, die ausgeführt wird, wenn das Modul mit dem -m-Schalter von Python übergeben wird. Damit dies funktioniert, muss das Modul runpy modifiziert werden, um die in PEP 451 eingeführte Modul-Spec-basierte Ladung zu nutzen. Außerdem muss ein Mechanismus zum Einrichten eines Moduls gemäß Slots, mit denen es ursprünglich nicht definiert wurde, hinzugefügt werden.

Implementierung

Eine WIP-Implementierung ist in einem GitHub-Repository [3] verfügbar; ein Patchset befindet sich unter [4].

Vorherige Ansätze

Stefan Behnel's initiales Proto-PEP [1] hatte einen "PyInit_modulename"-Hook, der eine Modulklasse erstellen würde, deren __init__ dann aufgerufen würde, um das Modul zu erstellen. Dieser Vorschlag entsprach nicht dem (damals nicht existenten) PEP 451, bei dem die Modulerstellung und -initialisierung in separate Schritte unterteilt ist. Er unterstützte auch nicht das Laden einer Erweiterung in bereits vorhandene Modulobjekte.

Alyssa (Nick) Coghlan schlug die "Create"- und "Exec"-Hooks vor und schrieb eine Prototypimplementierung [2]. Zu dieser Zeit war PEP 451 noch nicht implementiert, daher verwendete der Prototyp kein ModuleSpec.

Die ursprüngliche Version dieses PEP verwendete "Create"- und "Exec"-Hooks und erlaubte das Laden in beliebige vorkonstruierte Objekte mit dem "Exec"-Hook. Der Vorschlag brachte die Initialisierung von Erweiterungsmodulen näher an die Art und Weise, wie Python-Module initialisiert werden, aber später wurde erkannt, dass dies kein wichtiges Ziel ist. Der aktuelle PEP beschreibt eine einfachere Lösung.

Eine weitere Iteration verwendete einen "PyModuleExport"-Hook als Alternative zu PyInit, wobei PyInit für das bestehende Schema und PyModuleExport für Multi-Phase verwendet wurde. Da der Hook-Name jedoch nicht anhand des Modulnamens bestimmt werden konnte, erschwerte dies die automatische Generierung von PyImport_Inittab durch Tools wie freeze. Das Beibehalten nur des PyInit-Hook-Namens, auch wenn er nicht ganz für den Export einer Definition geeignet ist, ergab eine wesentlich einfachere Lösung.

Referenzen


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

Zuletzt geändert: 2025-10-07 15:05:23 GMT