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

Python Enhancement Proposals

PEP 573 – Zugriff auf Modulzustand aus C-Erweiterungsmethoden

Autor:
Petr Viktorin <encukou at gmail.com>, Alyssa Coghlan <ncoghlan at gmail.com>, Eric Snow <ericsnowcurrently at gmail.com>, Marcel Plch <gmarcel.plch at gmail.com>
BDFL-Delegate:
Stefan Behnel
Discussions-To:
Import-SIG Liste
Status:
Final
Typ:
Standards Track
Erstellt:
02-Jun-2016
Python-Version:
3.9
Post-History:


Inhaltsverzeichnis

Zusammenfassung

Dieser PEP schlägt vor, eine Möglichkeit für CPython-Erweiterungsmethoden hinzuzufügen, auf den Kontext zuzugreifen, z. B. den Zustand der Module, in denen sie definiert sind.

Dies wird es Erweiterungsmethoden ermöglichen, direkte Zeigerdereferenzierungen anstelle von `PyState_FindModule` zum Nachschlagen des Modulzustands zu verwenden, wodurch die Leistungskosten für die Verwendung modulspezifischen Zustands gegenüber prozessglobalem Zustand reduziert oder eliminiert werden.

Dies behebt eine der verbleibenden Hürden für die Übernahme von PEP 3121 (Initialisierung und Finalisierung von Erweiterungsmodulen) und PEP 489 (Mehrphasige Initialisierung von Erweiterungsmodulen).

Obwohl dieser PEP einen zusätzlichen Schritt zur vollständigen Lösung der Probleme unternimmt, die PEP 3121 und PEP 489 zu behandeln begonnen haben, versucht er nicht, *alle* verbleibenden Bedenken auszuräumen. Insbesondere der Zugriff auf den Modulzustand aus Slot-Methoden (`nb_add` usw.) ist nicht gelöst.

Terminologie

Prozessglobaler Zustand

C-seitige statische Variablen. Da dies eine sehr Low-Level-Speicherung ist, muss sie sorgfältig verwaltet werden.

Pro Modulzustand

Zustand, der auf ein Modulobjekt beschränkt ist und dynamisch als Teil der Initialisierung eines Modulobjekts alloziert wird. Dies isoliert den Zustand von anderen Instanzen des Moduls (einschließlich denen in anderen Subinterpretern).

Zugänglich über PyModule_GetState().

Statischer Typ

Ein Typobjekt, das als C-seitige statische Variable definiert ist, d. h. ein fest einkompiliertes Typobjekt.

Ein statischer Typ muss zwischen Modulinstanzen geteilt werden und hat keine Informationen darüber, zu welchem Modul er gehört. Statische Typen haben kein `__dict__` (obwohl ihre Instanzen dies haben könnten).

Heap-Typ

Ein Typobjekt, das zur Laufzeit erstellt wird.

Definierende Klasse

Die definierende Klasse einer Methode (entweder gebunden oder ungebunden) ist die Klasse, auf der die Methode definiert wurde. Eine Klasse, die die Methode lediglich von ihrer Basis erbt, ist nicht die definierende Klasse.

Zum Beispiel ist int die definierende Klasse von True.to_bytes, True.__floor__ und int.__repr__.

In C ist die definierende Klasse diejenige, die mit dem entsprechenden Eintrag in `tp_methods` oder "tp slots" [1] definiert wurde. Für in Python definierte Methoden wird die definierende Klasse in der `__class__` Closure-Zelle gespeichert.

C-API

Die "Python/C API", wie in der Python-Dokumentation beschrieben. CPython implementiert die C-API, aber andere Implementierungen existieren.

Begründung

PEP 489 hat eine neue Methode zur Initialisierung von Erweiterungsmodulen eingeführt, die mehrere Vorteile für Erweiterungen mit sich bringt, die sie implementieren

  • Die Erweiterungsmodule verhalten sich mehr wie ihre Python-Gegenstücke.
  • Die Erweiterungsmodule können das Laden in bereits vorhandene Modulobjekte problemlos unterstützen, was den Weg für die Unterstützung von Erweiterungsmodulen für runpy oder für Systeme ebnet, die das erneute Laden von Erweiterungsmodulen ermöglichen.
  • Das Laden mehrerer Module aus derselben Erweiterung ist möglich, was die Überprüfung der Modulisolation (ein Schlüsselmerkmal für die korrekte Unterstützung von Subinterpretern) aus einem einzigen Interpreter heraus ermöglicht.

Die größte Hürde für die Übernahme von PEP 489 ist die Ermöglichung des Zugriffs auf den Modulzustand von Methoden von Erweiterungstypen. Derzeit erfolgt der Zugriff auf diesen Zustand von Erweiterungsmethoden durch Nachschlagen des Moduls über PyState_FindModule (im Gegensatz zu Funktionen auf Modulebene in Erweiterungsmodulen, die eine Modulreferenz als Argument erhalten). `PyState_FindModule` fragt jedoch den Thread-lokalen Zustand ab, was es im Vergleich zum prozessglobalen Zugriff auf C-Ebene relativ kostspielig macht und folglich Modulautoren von der Verwendung abhält.

Außerdem stützt sich PyState_FindModule auf die Annahme, dass in jedem Subinterpreter höchstens ein Modul einem bestimmten PyModuleDef entspricht. Diese Annahme gilt nicht für Module, die die mehrphasige Initialisierung von PEP 489 verwenden, sodass PyState_FindModule für diese Module nicht verfügbar ist.

Eine schnellere und sicherere Methode für den Zugriff auf modulspezifischen Zustand von Erweiterungsmethoden wird benötigt.

Hintergrund

Die Implementierung einer Python-Methode kann Zugriff auf eine oder mehrere der folgenden Informationen benötigen

  • Die Instanz, auf der sie aufgerufen wird (`self`)
  • Die zugrundeliegende Funktion
  • Die *definierende Klasse*, d. h. die Klasse, in der die Methode definiert wurde
  • Das entsprechende Modul
  • Der Modulzustand

Im Python-Code können die entsprechenden Äquivalente auf Python-Ebene abgerufen werden als

import sys

class Foo:
    def meth(self):
        instance = self
        module_globals = globals()
        module_object = sys.modules[__name__]  # (1)
        underlying_function = Foo.meth         # (1)
        defining_class = Foo                   # (1)
        defining_class = __class__             # (2)

Hinweis

Die definierende Klasse ist nicht type(self), da type(self) eine Unterklasse von Foo sein könnte.

Die mit (1) gekennzeichneten Anweisungen stützen sich implizit auf namensbasierten Lookup über das `__globals__` der Funktion: entweder das `Foo`-Attribut, um auf die definierende Klasse und das Python-Funktionsobjekt zuzugreifen, oder `__name__`, um das Modulobjekt in `sys.modules` zu finden.

Im Python-Code ist dies machbar, da `__globals__` beim Ausführen der Funktionsdefinition entsprechend gesetzt wird, und selbst wenn der Namensraum manipuliert wurde, um ein anderes Objekt zurückzugeben, wird im schlimmsten Fall eine Ausnahme ausgelöst.

Die `__class__`-Closure, (2), ist eine sicherere Methode, um die definierende Klasse zu erhalten, aber sie stützt sich immer noch darauf, dass `__closure__` entsprechend gesetzt ist.

Im Gegensatz dazu sind Erweiterungsmethoden typischerweise als normale C-Funktionen implementiert. Das bedeutet, dass sie nur Zugriff auf ihre Argumente sowie auf den C-seitigen Thread-lokalen und prozessglobalen Zustand haben. Traditionell haben viele Erweiterungsmodule ihren gemeinsamen Zustand in C-seitigen Prozessglobalen gespeichert, was zu Problemen führt, wenn

  • mehrere Initialisierungs-/Finalisierungszyklen im selben Prozess ausgeführt werden
  • Module neu geladen werden (z. B. zum Testen bedingter Imports)
  • Erweiterungsmodule in Subinterpretern geladen werden

PEP 3121 versuchte dies zu lösen, indem die `PyState_FindModule` API angeboten wurde, aber diese hat immer noch erhebliche Probleme mit Erweiterungsmethoden (im Gegensatz zu Funktionen auf Modulebene)

  • sie ist merklich langsamer als der direkte Zugriff auf prozessglobalen Zustand auf C-Ebene
  • es gibt immer noch eine gewisse inhärente Abhängigkeit vom prozessglobalen Zustand, was bedeutet, dass sie das Neuladen von Modulen immer noch nicht zuverlässig handhabt

Es ist auch so, dass bei der Suche nach einer C-seitigen Struktur wie dem Modulzustand die Angabe eines unerwarteten Objektlayouts den Interpreter zum Absturz bringen kann. Daher ist es deutlich wichtiger sicherzustellen, dass Erweiterungsmethoden die Art von Objekt erhalten, die sie erwarten.

Vorschlag

Derzeit erhält eine gebundene Erweiterungsmethode (`PyCFunction` oder `PyCFunctionWithKeywords`) nur `self` und (falls zutreffend) die übergebenen positions- und Schlüsselwortargumente.

Während auf Modulebene befindliche Erweiterungsfunktionen bereits über ihr `self`-Argument Zugriff auf das definierende Modulobjekt erhalten, haben Methoden von Erweiterungstypen diesen Luxus nicht: Sie erhalten die gebundene Instanz über `self` und haben daher keinen direkten Zugriff auf die definierende Klasse oder den Modulzustand.

Der zusätzliche modulspezifische Kontext, der oben beschrieben wurde, kann mit zwei Änderungen verfügbar gemacht werden. Beide Änderungen sind optional; Erweiterungsautoren müssen sich aktiv dafür entscheiden, um sie nutzen zu können.

  • Fügen Sie einen Zeiger auf das Modul zu Heap-Typ-Objekten hinzu.
  • Übergeben Sie die definierende Klasse an die zugrundeliegende C-Funktion.

    In CPython ist die definierende Klasse zum Zeitpunkt der Erstellung des integrierten Methodenobjekts (`PyCFunctionObject`) leicht verfügbar, sodass sie in einer neuen Struktur gespeichert werden kann, die `PyCFunctionObject` erweitert.

Der Modulzustand kann dann vom Modulobjekt über `PyModule_GetState` abgerufen werden.

Beachten Sie, dass dieser Vorschlag impliziert, dass jeder Typ, dessen Methoden auf pro Modulzustand zugreifen müssen, ein Heap-Typ und kein statischer Typ sein muss. Dies ist notwendig, um das Laden mehrerer Modulobjekte aus einer einzigen Erweiterung zu unterstützen: Ein statischer Typ als C-seitiges globales Objekt hat keine Informationen darüber, zu welchem Modulobjekt er gehört.

Slot-Methoden

Die oben genannten Änderungen decken keine Slot-Methoden wie `tp_iter` oder `nb_add` ab.

Das Problem bei Slot-Methoden ist, dass ihre C-API fest ist, sodass wir nicht einfach ein neues Argument hinzufügen können, um die definierende Klasse zu übergeben. Zwei mögliche Lösungen wurden für dieses Problem vorgeschlagen:

  • Suchen Sie die Klasse, indem Sie den MRO durchlaufen. Dies ist potenziell teuer, wird aber verwendbar sein, wenn die Leistung keine Rolle spielt (z. B. beim Auslösen einer Ausnahme auf Modulebene).
  • Speichern eines Zeigers auf die definierende Klasse jedes Slots in einer separaten Tabelle, `__typeslots__` [2]. Dies ist technisch machbar und schnell, aber ziemlich invasiv.

Module, die von diesem Problem betroffen sind, haben auch die Möglichkeit, Thread-lokalen Zustand oder PEP 567 Kontextvariablen als Caching-Mechanismus zu verwenden oder ein eigenes Cache-Schema zur Neuladung zu definieren.

Die allgemeine Lösung des Problems wird auf einen zukünftigen PEP verschoben.

Spezifikation

Hinzufügen von Modulreferenzen zu Heap-Typen

Eine neue Factory-Methode wird der C-API zum Erstellen von Modulen hinzugefügt

PyObject* PyType_FromModuleAndSpec(PyObject *module,
                                   PyType_Spec *spec,
                                   PyObject *bases)

Diese verhält sich wie PyType_FromSpecWithBases und ordnet zusätzlich das bereitgestellte Modulobjekt dem neuen Typ zu. (In CPython setzt dies `ht_module` wie unten beschrieben.)

Zusätzlich wird ein Akzessor, PyObject * PyType_GetModule(PyTypeObject *), bereitgestellt. Er gibt das mit dem Typ verbundene Modul zurück, falls vorhanden, andernfalls wird TypeError gesetzt und NULL zurückgegeben. Wenn ein statischer Typ übergeben wird, wird immer TypeError gesetzt und NULL zurückgegeben.

Zur Implementierung in CPython erhält die Struktur `PyHeapTypeObject` ein neues Mitglied, `PyObject *ht_module`, das einen Zeiger auf das zugehörige Modul speichert. Es wird standardmäßig auf `NULL` gesetzt und sollte nach der Erstellung des Typobjekts nicht mehr geändert werden.

Das Mitglied `ht_module` wird nicht an Unterklassen vererbt; es muss für jeden einzelnen Typ, der es benötigt, mit `PyType_FromSpecWithBases` gesetzt werden.

Normalerweise führt die Erstellung einer Klasse mit gesetztem `ht_module` zu einem Referenzzyklus, der die Klasse und das Modul umfasst. Dies ist kein Problem, da das Herunterfahren von Modulen kein leistungskritischer Vorgang ist und Modulfunktionen typischerweise ebenfalls Referenzzyklen erzeugen. Der bestehende Code zum "Setzen aller Modulglobalen auf None", der Funktionszyklen über `f_globals` durchbricht, wird auch die neuen Zyklen über `ht_module` durchbrechen.

Übergabe der definierenden Klasse an Erweiterungsmethoden

Ein neues Signaturflag, `METH_METHOD`, wird für die Verwendung in `PyMethodDef.ml_flags` hinzugefügt. Konzeptionell fügt es `defining_class` zur Funktionssignatur hinzu. Um die anfängliche Implementierung zu erleichtern, kann das Flag nur als `(METH_FASTCALL | METH_KEYWORDS | METH_METHOD)` verwendet werden. (Es kann nicht mit anderen Flags wie `METH_O` oder einfachem `METH_FASTCALL` verwendet werden, kann aber mit `METH_CLASS` oder `METH_STATIC` kombiniert werden).

C-Funktionen für Methoden, die mit dieser Flag-Kombination definiert wurden, werden über eine neue C-Signatur namens `PyCMethod` aufgerufen.

PyObject *PyCMethod(PyObject *self,
                    PyTypeObject *defining_class,
                    PyObject *const *args,
                    size_t nargsf,
                    PyObject *kwnames)

Weitere Kombinationen wie `(METH_VARARGS | METH_METHOD)` können zukünftig hinzugefügt werden (oder bereits in der anfänglichen Implementierung dieses PEP). `METH_METHOD` sollte jedoch immer ein *zusätzliches* Flag sein, d. h. die definierende Klasse sollte nur übergeben werden, wenn sie benötigt wird.

In CPython wird eine neue Struktur, die `PyCFunctionObject` erweitert, hinzugefügt, um die zusätzlichen Informationen zu speichern.

typedef struct {
    PyCFunctionObject func;
    PyTypeObject *mm_class; /* Passed as 'defining_class' arg to the C func */
} PyCMethodObject;

Die `PyCFunction`-Implementierung übergibt `mm_class` an eine `PyCMethod`-C-Funktion, wenn sie feststellt, dass das `METH_METHOD`-Flag gesetzt ist. Ein neues Makro `PyCFunction_GET_CLASS(cls)` wird für einen einfacheren Zugriff auf `mm_class` hinzugefügt.

C-Methoden können weiterhin die anderen `METH_*`-Signaturen verwenden, wenn sie keinen Zugriff auf ihre definierende Klasse/ihr Modul benötigen. Wenn `METH_METHOD` nicht gesetzt ist, ist das Umwandeln in `PyCMethodObject` ungültig.

Argument Clinic

Um die Übergabe der definierenden Klasse an Methoden, die Argument Clinic verwenden, zu unterstützen, wird ein neuer Konverter namens `defining_class` zum Argument Clinic-Tool von CPython hinzugefügt.

Jede Methode kann nur ein Argument mit diesem Konverter verwenden, und es muss nach `self` oder, falls `self` nicht verwendet wird, als erstes Argument erscheinen. Das Argument hat den Typ `PyTypeObject *`.

Bei Verwendung wählt Argument Clinic `METH_FASTCALL | METH_KEYWORDS | METH_METHOD` als Aufrufkonvention. Das Argument wird nicht in `__text_signature__` erscheinen.

Der neue Konverter wird anfänglich nicht mit `__init__` und `__new__`-Methoden kompatibel sein, die die `METH_METHOD`-Konvention nicht verwenden können.

Helfer

Der Zugriff auf den pro Modulzustand von einem Heap-Typ aus ist eine sehr häufige Aufgabe. Um dies zu erleichtern, wird ein Helfer hinzugefügt.

void *PyType_GetModuleState(PyObject *type)

Diese Funktion nimmt einen Heap-Typ entgegen und gibt im Erfolgsfall einen Zeiger auf den Zustand des Moduls zurück, zu dem der Heap-Typ gehört.

Bei einem Fehler können zwei Szenarien eintreten. Wenn ein Nicht-Typ-Objekt oder ein Typ ohne Modul übergeben wird, wird `TypeError` gesetzt und `NULL` zurückgegeben. Wenn das Modul gefunden wird, wird der Zeiger auf den Zustand, der `NULL` sein kann, zurückgegeben, ohne eine Ausnahme zu setzen.

Module, die in der anfänglichen Implementierung konvertiert wurden

Zur Validierung des Ansatzes wird das Modul `_elementtree` während der anfänglichen Implementierung modifiziert.

Zusammenfassung der API-Änderungen und -Ergänzungen

Folgendes wird der Python C-API hinzugefügt:

  • Funktion `PyType_FromModuleAndSpec`
  • Funktion `PyType_GetModule`
  • Funktion `PyType_GetModuleState`
  • Aufruf-Flag `METH_METHOD`
  • Funktionssignatur `PyCMethod`

Folgende Ergänzungen werden als CPython-Implementierungsdetails hinzugefügt und nicht dokumentiert:

  • Makro `PyCFunction_GET_CLASS`
  • Struktur `PyCMethodObject`
  • Mitglied `ht_module` von `_heaptypeobject`
  • Konverter `defining_class` in Argument Clinic

Abwärtskompatibilität

Ein neuer Zeiger wird zu allen Heap-Typen hinzugefügt. Alle anderen Änderungen sind das Hinzufügen neuer Funktionen und Strukturen oder Änderungen an privaten Implementierungsdetails.

Implementierung

Eine anfängliche Implementierung ist in einem Github-Repository [3] verfügbar; ein Patchset befindet sich unter [4].

Mögliche zukünftige Erweiterungen

Slot-Methoden

Eine Möglichkeit zur Übergabe der definierenden Klasse (oder des Modulzustands) an Slot-Methoden kann zukünftig hinzugefügt werden.

Eine frühere Version dieses PEP schlug eine Hilfsfunktion vor, die eine definierende Klasse durch Suchen des MRO nach einer Klasse, die einen Slot für eine bestimmte Funktion definiert, ermittelt. Dieser Ansatz würde jedoch fehlschlagen, wenn eine Klasse mutiert wird (was für Heap-Typen von Python-Code aus möglich ist). Die Lösung dieses Problems wird zukünftigen Diskussionen überlassen.

Einfache Erstellung von Typen mit Modulreferenzen

Es wäre möglich, einen Ausführungsslot-Typ nach PEP 489 hinzuzufügen, um die Erstellung von Heap-Typen erheblich einfacher zu machen als den Aufruf von `PyType_FromModuleAndSpec`. Dies wird einem zukünftigen PEP überlassen.

Es könnte gut sein, eine gute Möglichkeit hinzuzufügen, statische Ausnahmetypen aus der begrenzten API zu erstellen. Solche Ausnahmetypen könnten zwischen Subinterpretern geteilt werden, aber ohne spezifischen Modulzustand instanziiert werden. Dies wird ebenfalls zukünftigen Diskussionen überlassen.

Optimierung

Wie hier vorgeschlagen, unterstützen Methoden, die mit dem `METH_METHOD`-Flag definiert wurden, nur eine spezifische Signatur.

Wenn sich herausstellt, dass aus Leistungsgründen andere Signaturen benötigt werden, können diese hinzugefügt werden.

Referenzen


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

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