PEP 253 – Subtypen von eingebauten Typen
- Autor:
- Guido van Rossum <guido at python.org>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 14. Mai 2001
- Python-Version:
- 2.2
- Post-History:
Inhaltsverzeichnis
- Zusammenfassung
- Einleitung
- Über Metatypen
- Einen Typ als Fabrik für seine Instanzen machen
- Einen Typ für die Subtypisierung vorbereiten
- Einen Subtyp eines eingebauten Typs in C erstellen
- Subtypisierung in Python
- Mehrfachvererbung
- MRO: Method Resolution Order (die Lookup-Regel)
- XXX Zu erledigen
- Implementierung
- Referenzen
- Urheberrecht
Zusammenfassung
Dieser PEP schlägt Erweiterungen für die Typobjekt-API vor, die die Erstellung von Subtypen eingebauter Typen in C und in Python ermöglichen.
[Redaktioneller Hinweis: Die in diesem PEP beschriebenen Ideen wurden in Python integriert. Der PEP beschreibt die Implementierung nicht mehr korrekt.]
Einleitung
Traditionell werden Typen in Python statisch erstellt, indem eine globale Variable vom Typ PyTypeObject deklariert und mit einem statischen Initialisierer initialisiert wird. Die Slots im Typobjekt beschreiben alle Aspekte eines Python-Typs, die für den Python-Interpreter relevant sind. Einige Slots enthalten Dimensionsinformationen (wie die grundlegende Allokationsgröße von Instanzen), andere enthalten verschiedene Flags, aber die meisten Slots sind Zeiger auf Funktionen, die verschiedene Verhaltensweisen implementieren. Ein NULL-Zeiger bedeutet, dass der Typ das spezifische Verhalten nicht implementiert; in diesem Fall kann das System ein Standardverhalten bereitstellen oder eine Ausnahme auslösen, wenn das Verhalten für eine Instanz des Typs aufgerufen wird. Einige Sammlungen von Funktionszeigern, die normalerweise zusammen definiert sind, werden indirekt über einen Zeiger auf eine zusätzliche Struktur mit weiteren Funktionszeigern erhalten.
Obwohl die Details der Initialisierung einer PyTypeObject-Struktur nicht als solche dokumentiert sind, lassen sie sich leicht aus den Beispielen im Quellcode ableiten, und ich gehe davon aus, dass der Leser mit der traditionellen Methode zur Erstellung neuer Python-Typen in C ausreichend vertraut ist.
Dieser PEP führt folgende Funktionen ein:
- Ein Typ kann eine Fabrikfunktion für seine Instanzen sein
- Typen können in C subtypisiert werden
- Typen können in Python mit der class-Anweisung subtypisiert werden
- Mehrfachvererbung von Typen wird unterstützt (soweit praktisch – Sie können immer noch nicht von list und dictionary mehrfach erben)
- Die Standard-Kooperationsfunktionen (int, tuple, str usw.) werden zu den entsprechenden Typobjekten neu definiert, die als ihre eigenen Fabrikfunktionen dienen
- Eine class-Anweisung kann eine
__metaclass__Deklaration enthalten, die die zu verwendende Metaklasse zur Erstellung der neuen Klasse angibt - Eine class-Anweisung kann eine
__slots__Deklaration enthalten, die die spezifischen Namen der unterstützten Instanzvariablen angibt
Dieser PEP baut auf PEP 252 auf, der eine standardmäßige Introspektion für Typen hinzufügt; wenn zum Beispiel ein bestimmter Typobjekt den tp_hash Slot initialisiert, hat dieses Typobjekt eine __hash__ Methode bei der Introspektion. PEP 252 fügt auch ein Dictionary zu Typobjekten hinzu, das alle Methoden enthält. Auf Python-Ebene ist dieses Dictionary für eingebaute Typen schreibgeschützt; auf C-Ebene ist es direkt zugänglich (sollte aber nicht modifiziert werden, außer als Teil der Initialisierung).
Für die Binärkompatibilität zeigt ein Flag-Bit im tp_flags Slot die Existenz der verschiedenen neuen Slots im Typobjekt an, die unten eingeführt werden. Typen, die nicht das Py_TPFLAGS_HAVE_CLASS Bit in ihrem tp_flags Slot gesetzt haben, werden angenommen, NULL-Werte für alle Subtyp-Slots zu haben. (Warnung: Der aktuelle Implementierungsprototyp ist in seiner Überprüfung dieses Flag-Bits noch nicht konsistent. Dies sollte vor der endgültigen Veröffentlichung behoben werden.)
Im aktuellen Python wird zwischen Typen und Klassen unterschieden. Dieser PEP wird zusammen mit PEP 254 diese Unterscheidung aufheben. Aus Kompatibilitätsgründen wird die Unterscheidung jedoch wahrscheinlich noch viele Jahre bestehen bleiben, und ohne PEP 254 ist die Unterscheidung immer noch groß: Typen haben letztendlich einen eingebauten Typ als Basisklasse, während Klassen letztendlich von einer benutzerdefinierten Klasse abgeleitet sind. Daher werde ich im Folgenden das Wort Typ verwenden, wann immer ich kann – einschließlich Basis-Typ oder Obertyp, abgeleiteter Typ oder Untertyp und Metatyp. Manchmal vermischt sich die Terminologie jedoch zwangsläufig, zum Beispiel wird der Typ eines Objekts durch sein __class__ Attribut bestimmt, und Subtypisierung in Python wird mit einer class-Anweisung geschrieben. Wenn eine weitere Unterscheidung notwendig ist, können benutzerdefinierte Klassen als „klassische“ Klassen bezeichnet werden.
Über Metatypen
Unvermeidlich kommt die Diskussion zu Metatypen (oder Metaklassen). Metatypen sind in Python nichts Neues: Python kann schon immer über den Typ eines Typs sprechen
>>> a = 0
>>> type(a)
<type 'int'>
>>> type(type(a))
<type 'type'>
>>> type(type(type(a)))
<type 'type'>
>>>
In diesem Beispiel ist type(a) ein „regulärer“ Typ und type(type(a)) ist ein Metatyp. Während bei der Auslieferung alle Typen denselben Metatyp haben (PyType_Type, der auch sein eigener Metatyp ist), ist dies keine Anforderung, und tatsächlich erstellt eine nützliche und relevante 3rd-Party-Erweiterung (ExtensionClasses von Jim Fulton) einen zusätzlichen Metatyp. Der Typ klassischer Klassen, bekannt als types.ClassType, kann ebenfalls als ein eigenständiger Metatyp betrachtet werden.
Eine enge Verbindung zu Metatypen hat der „Don Beaudry Hook“, der besagt, dass wenn ein Metatyp aufrufbar ist, seine Instanzen (die reguläre Typen sind) mittels einer Python class-Anweisung untervererbt (wirklich subtypisiert) werden können. Ich werde diese Regel verwenden, um die Subtypisierung von eingebauten Typen zu unterstützen, und tatsächlich vereinfacht sie die Logik der Klassenerstellung erheblich, indem sie einfach immer den Metatyp aufruft. Wenn keine Basisklasse angegeben ist, wird ein Standard-Metatyp aufgerufen – der Standard-Metatyp ist das „ClassType“-Objekt, sodass die class-Anweisung im normalen Fall wie bisher verhält. (Dieser Standard kann pro Modul geändert werden, indem die globale Variable __metaclass__ gesetzt wird.)
Python verwendet das Konzept von Metatypen oder Metaklassen anders als Smalltalk. In Smalltalk-80 gibt es eine Hierarchie von Metaklassen, die die Hierarchie von regulären Klassen widerspiegelt, Metaklassen entsprechen 1:1 Klassen (abgesehen von einigen seltsamen Dingen an der Wurzel der Hierarchie), und jede class-Anweisung erstellt sowohl eine reguläre Klasse als auch ihre Metaklasse, wobei Klassenmethoden in die Metaklasse und Instanzmethoden in die reguläre Klasse gelegt werden.
So nett das im Kontext von Smalltalk auch sein mag, es ist nicht kompatibel mit der traditionellen Verwendung von Metatypen in Python, und ich ziehe es vor, im Python-Stil fortzufahren. Dies bedeutet, dass Python-Metatypen typischerweise in C geschrieben sind und zwischen vielen regulären Typen geteilt werden können. (Es wird möglich sein, Metatypen in Python zu subtypisieren, sodass es nicht unbedingt notwendig sein wird, C zu schreiben, um Metatypen zu verwenden; aber die Leistungsfähigkeit von Python-Metatypen wird begrenzt sein. Zum Beispiel wird Python-Code niemals erlaubt sein, rohen Speicher zu allozieren und ihn nach Belieben zu initialisieren.)
Metatypen bestimmen verschiedene **Richtlinien** für Typen, wie z.B. was passiert, wenn ein Typ aufgerufen wird, wie dynamisch Typen sind (ob das __dict__ eines Typs nach seiner Erstellung modifiziert werden kann), was die Method Resolution Order ist, wie Instanzattribute nachgeschlagen werden und so weiter.
Ich werde argumentieren, dass Tiefensuche von links nach rechts keine gute Lösung ist, wenn man die Mehrfachvererbung am besten nutzen möchte.
Ich werde argumentieren, dass bei Mehrfachvererbung der Metatyp des Subtyps ein Nachfahre der Metatypen aller Basistypen sein muss.
Ich werde später auf Metatypen zurückkommen.
Einen Typ als Fabrik für seine Instanzen machen
Traditionell gibt es für jeden Typ mindestens eine C-Fabrikfunktion, die Instanzen des Typs erstellt (PyTuple_New(), PyInt_FromLong() und so weiter). Diese Fabrikfunktionen kümmern sich sowohl um die Speicherallokation für das Objekt als auch um die Initialisierung dieses Speichers. Seit Python 2.0 müssen sie auch mit dem Garbage-Collection-Subsystem interagieren, wenn der Typ an der Garbage Collection teilnehmen möchte (was optional ist, aber für sogenannte „Container“-Typen dringend empfohlen wird: Typen, die Verweise auf andere Objekte enthalten können und somit an Referenzzyklen teilnehmen können).
In diesem Vorschlag können Typobjekte Fabrikfunktionen für ihre Instanzen sein, wodurch die Typen direkt von Python aus aufrufbar werden. Dies ahmt die Art und Weise nach, wie Klassen instanziiert werden. Die C-APIs zur Erstellung von Instanzen verschiedener eingebauter Typen bleiben gültig und sind in einigen Fällen effizienter. Nicht alle Typen werden zu ihren eigenen Fabrikfunktionen.
Das Typobjekt hat einen neuen Slot, tp_new, der als Fabrik für Instanzen des Typs fungieren kann. Typen sind nun aufrufbar, da der tp_call Slot in PyType_Type (dem Metatyp) gesetzt ist; die Funktion sucht nach dem tp_new Slot des Typs, der aufgerufen wird.
Erklärung: Der tp_call Slot eines regulären Typobjekts (wie PyInt_Type oder PyList_Type) definiert, was passiert, wenn **Instanzen** dieses Typs aufgerufen werden; insbesondere ist der tp_call Slot im Funktionstyp, PyFunction_Type, der Schlüssel, um Funktionen aufrufbar zu machen. Als weiteres Beispiel ist PyInt_Type.tp_call NULL, da ganze Zahlen nicht aufrufbar sind. Das neue Paradigma macht **Typobjekte** aufrufbar. Da Typobjekte Instanzen ihres Metatyps (PyType_Type) sind, zeigt der tp_call Slot des Metatyps (PyType_Type.tp_call) auf eine Funktion, die aufgerufen wird, wenn ein Typobjekt aufgerufen wird. Da nun jeder Typ etwas anderes tun muss, um eine Instanz von sich selbst zu erstellen, verzweigt PyType_Type.tp_call sofort zum tp_new Slot des Typs, der aufgerufen wird. PyType_Type selbst ist ebenfalls aufrufbar: Sein tp_new Slot erstellt einen neuen Typ. Dies wird von der class-Anweisung verwendet (Formalisierung des Don Beaudry Hooks, siehe oben). Und was macht PyType_Type aufrufbar? Der tp_call Slot seines Metatyps – aber da es sein eigener Metatyp ist, ist das sein eigener tp_call Slot!
Wenn der tp_new Slot des Typs NULL ist, wird eine Ausnahme ausgelöst. Andernfalls wird der tp_new Slot aufgerufen. Die Signatur für den tp_new Slot ist
PyObject *tp_new(PyTypeObject *type,
PyObject *args,
PyObject *kwds)
wobei „type“ der Typ ist, dessen tp_new Slot aufgerufen wird, und „args“ und „kwds“ die sequenziellen und Schlüsselwortargumente des Aufrufs sind, unverändert von tp_call übergeben werden. (Das „type“-Argument wird in Kombination mit Vererbung verwendet, siehe unten.)
Es gibt keine Einschränkungen für den zurückgegebenen Objekttyp, obwohl er konventionsgemäß eine Instanz des gegebenen Typs sein sollte. Es ist nicht notwendig, dass ein neues Objekt zurückgegeben wird; ein Verweis auf ein vorhandenes Objekt ist ebenfalls in Ordnung. Der Rückgabewert sollte immer ein neuer Verweis sein, der dem Aufrufer gehört.
Sobald der tp_new Slot ein Objekt zurückgegeben hat, wird versucht, die weitere Initialisierung durch Aufruf des tp_init() Slots des Typs des resultierenden Objekts durchzuführen, falls dieser nicht NULL ist. Dies hat die folgende Signatur:
int tp_init(PyObject *self,
PyObject *args,
PyObject *kwds)
Dies entspricht stärker der __init__() Methode von klassischen Klassen und wird tatsächlich durch die Slot/Sonder-Methoden-Korrespondenzregeln darauf abgebildet. Der Unterschied in den Verantwortlichkeiten zwischen dem tp_new() Slot und dem tp_init() Slot liegt in den von ihnen gewährleisteten Invarianten. Der tp_new() Slot sollte nur die wesentlichsten Invarianten gewährleisten, ohne die der C-Code, der die Objekte implementiert, fehlschlagen würde. Der tp_init() Slot sollte für überschreibbare benutzerspezifische Initialisierungen verwendet werden. Nehmen Sie zum Beispiel den Dictionary-Typ. Die Implementierung hat einen internen Zeiger auf eine Hashtabelle, die niemals NULL sein darf. Diese Invariante wird durch den tp_new() Slot für Dictionaries gewährleistet. Der Dictionary tp_init() Slot könnte hingegen verwendet werden, um dem Dictionary einen anfänglichen Satz von Schlüsseln und Werten basierend auf den übergebenen Argumenten zu geben.
Beachten Sie, dass für unveränderliche Objekttypen die Initialisierung nicht durch den tp_init() Slot erfolgen kann: Dies würde dem Python-Benutzer eine Möglichkeit geben, die Initialisierung zu ändern. Daher haben unveränderliche Objekte typischerweise eine leere tp_init() Implementierung und führen ihre gesamte Initialisierung in ihrem tp_new() Slot durch.
Sie fragen sich vielleicht, warum der tp_new() Slot nicht den tp_init() Slot selbst aufrufen sollte. Der Grund dafür ist, dass es unter bestimmten Umständen (wie der Unterstützung persistenter Objekte) wichtig ist, ein Objekt eines bestimmten Typs erstellen zu können, ohne es weiter zu initialisieren als unbedingt nötig. Dies kann bequem durch Aufruf des tp_new() Slots ohne Aufruf von tp_init() erfolgen. Es ist auch möglich, dass tp_init() nicht aufgerufen wird oder mehr als einmal aufgerufen wird – seine Operation sollte auch in diesen anomalen Fällen robust sein.
Für einige Objekte kann tp_new() ein vorhandenes Objekt zurückgeben. Beispielsweise speichert die Fabrikfunktion für ganze Zahlen die Zahlen -1 bis 99 im Cache. Dies ist nur zulässig, wenn das Typ-Argument für tp_new() der Typ ist, der die tp_new() Funktion definiert hat (im Beispiel, wenn type == &PyInt_Type) und wenn der tp_init() Slot für diesen Typ nichts tut. Wenn das Typ-Argument abweicht, wird der Aufruf von tp_new() von der tp_new() eines abgeleiteten Typs initiiert, um das Objekt zu erstellen und den Basis-Typ-Teil des Objekts zu initialisieren; in diesem Fall sollte tp_new() immer ein neues Objekt zurückgeben (oder eine Ausnahme auslösen).
Sowohl tp_new() als auch tp_init() sollten exakt dieselben ‚args‘ und ‚kwds‘ Argumente erhalten und beide sollten überprüfen, ob die Argumente akzeptabel sind, da sie unabhängig voneinander aufgerufen werden können.
Es gibt einen dritten Slot im Zusammenhang mit der Objekterstellung: tp_alloc(). Seine Aufgabe ist es, den Speicher für das Objekt zu allozieren, die Referenzanzahl (ob_refcnt) und den Typzeiger (ob_type) zu initialisieren und den Rest des Objekts auf Null zu setzen. Es sollte das Objekt auch beim Garbage-Collection-Subsystem registrieren, wenn der Typ Garbage Collection unterstützt. Dieser Slot existiert, damit abgeleitete Typen die Speicherallokationsrichtlinie (wie z.B. welcher Heap verwendet wird) getrennt vom Initialisierungscode überschreiben können. Die Signatur ist
PyObject *tp_alloc(PyTypeObject *type, int nitems)
Das Typ-Argument ist der Typ des neuen Objekts. Das nitems-Argument ist normalerweise Null, außer bei Objekten mit variabler Allokationsgröße (hauptsächlich Zeichenketten, Tupel und lange ganze Zahlen). Die Allokationsgröße wird durch den folgenden Ausdruck gegeben:
type->tp_basicsize + nitems * type->tp_itemsize
Der tp_alloc Slot wird nur für subtypisierbare Typen verwendet. Die tp_new() Funktion der Basisklasse muss den tp_alloc() Slot des Typs aufrufen, der als erstes Argument übergeben wird. Es ist die Verantwortung der tp_new() Funktion, die Anzahl der Elemente zu berechnen. Der tp_alloc() Slot setzt das ob_size Mitglied des neuen Objekts, wenn das type->tp_itemsize Mitglied ungleich Null ist.
(Hinweis: In bestimmten Debug-Kompilierungsmodi hatte die Typstruktur früher bereits Member namens tp_alloc und tp_free, Zähler für die Anzahl der Allokationen und Deallokationen. Diese werden in tp_allocs und tp_deallocs umbenannt.)
Standardimplementierungen für tp_alloc() und tp_new() sind verfügbar. PyType_GenericAlloc() alloziert ein Objekt aus dem Standard-Heap und initialisiert es ordnungsgemäß. Es verwendet die obige Formel zur Bestimmung der zu allozierenden Speichermenge und kümmert sich um die GC-Registrierung. Der einzige Grund, diese Implementierung nicht zu verwenden, wäre die Allokation von Objekten aus einem anderen Heap (wie es bei einigen sehr kleinen, häufig verwendeten Objekten wie ints und tuples der Fall ist). PyType_GenericNew() fügt nur sehr wenig hinzu: Es ruft lediglich den tp_alloc() Slot des Typs mit Null für nitems auf. Aber für mutable Typen, die ihre gesamte Initialisierung in ihrem tp_init() Slot durchführen, könnte dies genau das Richtige sein.
Einen Typ für die Subtypisierung vorbereiten
Die Idee hinter Subtypisierung ähnelt sehr der von Einfachvererbung in C++. Ein Basistyp wird durch eine Strukturdeklaration (ähnlich der C++-Klassendeklaration) plus einem Typobjekt (ähnlich der C++-Vtable) beschrieben. Ein abgeleiteter Typ kann die Struktur erweitern (muss aber Namen, Reihenfolge und Typ der Member der Basisstruktur unverändert lassen) und bestimmte Slots im Typobjekt überschreiben, während andere unverändert bleiben. (Im Gegensatz zu C++-Vtables haben alle Python-Typobjekte dieselbe Speicherlayout.)
Der Basistyp muss Folgendes tun:
- Den Flag-Wert
Py_TPFLAGS_BASETYPEzutp_flagshinzufügen. - Die Slots
tp_new(),tp_alloc()und optionaltp_init()deklarieren und verwenden. - Die Slots
tp_dealloc()undtp_free()deklarieren und verwenden. - Seine Objektstrukturdeklaration exportieren.
- Ein Subtyp-fähiges Typüberprüfungs-Makro exportieren.
Die Anforderungen und Signaturen für tp_new(), tp_alloc() und tp_init() wurden bereits oben besprochen: tp_alloc() sollte den Speicher allozieren und ihn größtenteils mit Nullen initialisieren; tp_new() sollte den tp_alloc() Slot aufrufen und dann mit der minimal erforderlichen Initialisierung fortfahren; tp_init() sollte für eine umfassendere Initialisierung von veränderlichen Objekten verwendet werden.
Es sollte keine Überraschung sein, dass es ähnliche Konventionen am Ende der Lebensdauer eines Objekts gibt. Die beteiligten Slots sind tp_dealloc() (allen bekannt, die jemals eine Python-Erweiterungstyps implementiert haben) und tp_free(), der Neue in der Runde. (Die Namen sind nicht ganz symmetrisch; tp_free() entspricht tp_alloc(), was in Ordnung ist, aber tp_dealloc() entspricht tp_new(). Vielleicht sollte der tp_dealloc Slot umbenannt werden?)
Der tp_free() Slot sollte zum Freigeben des Speichers und zur Abmeldung des Objekts beim Garbage-Collection-Subsystem verwendet werden und kann von einer abgeleiteten Klasse überschrieben werden; tp_dealloc() sollte das Objekt deinitialisieren (normalerweise durch Aufruf von Py_XDECREF() für verschiedene Unterobjekte) und dann tp_free() aufrufen, um den Speicher freizugeben. Die Signatur für tp_dealloc() ist dieselbe wie immer:
void tp_dealloc(PyObject *object)
Die Signatur für tp_free() ist dieselbe
void tp_free(PyObject *object)
(In einer früheren Version dieses PEP gab es auch eine Rolle für den tp_clear() Slot. Dies erwies sich als schlechte Idee.)
Um sinnvoll in C subtypisiert werden zu können, muss ein Typ die Strukturdeklaration seiner Instanzen über eine Header-Datei exportieren, da sie zur Ableitung eines Subtyps benötigt wird. Das Typobjekt für den Basistyp muss ebenfalls exportiert werden.
Wenn der Basistyp ein Typüberprüfungs-Makro hat (wie PyDict_Check()), sollte dieses Makro so angepasst werden, dass es auch Subtypen erkennt. Dies kann durch Verwendung des neuen PyObject_TypeCheck(object, type) Makros geschehen, das eine Funktion aufruft, die den Basisklassen-Links folgt.
Das PyObject_TypeCheck() Makro enthält eine leichte Optimierung: Es vergleicht zuerst direkt object->ob_type mit dem Typ-Argument, und wenn dies eine Übereinstimmung ist, wird der Funktionsaufruf umgangen. Dies sollte es für die meisten Situationen schnell genug machen.
Beachten Sie, dass diese Änderung des Typüberprüfungs-Makros bedeutet, dass C-Funktionen, die eine Instanz des Basistyps benötigen, mit Instanzen des abgeleiteten Typs aufgerufen werden können. Bevor die Subtypisierung eines bestimmten Typs aktiviert wird, sollte dessen Code überprüft werden, um sicherzustellen, dass dies nichts beschädigt. Es hat sich im Prototyp als nützlich erwiesen, ein weiteres Typüberprüfungs-Makro für die eingebauten Python-Objekttypen hinzuzufügen, um auch eine exakte Typübereinstimmung zu prüfen (z.B. ist PyDict_Check(x) wahr, wenn x eine Instanz eines Dictionaries oder einer Dictionary-Unterklasse ist, während PyDict_CheckExact(x) nur wahr ist, wenn x ein Dictionary ist).
Einen Subtyp eines eingebauten Typs in C erstellen
Die einfachste Form der Subtypisierung ist die Subtypisierung in C. Sie ist die einfachste Form, weil wir den C-Code dazu zwingen können, sich einiger Probleme bewusst zu sein, und es akzeptabel ist, dass C-Code, der die Regeln nicht befolgt, abstürzt. Zur zusätzlichen Vereinfachung ist sie auf Einfachvererbung beschränkt.
Nehmen wir an, wir leiten von einem veränderlichen Basistyp ab, dessen tp_itemsize Null ist. Der Subtyp-Code ist nicht GC-fähig, obwohl er GC-Fähigkeit vom Basistyp erben kann (dies geschieht automatisch). Die Allokation des Basistyps verwendet den Standard-Heap.
Der abgeleitete Typ beginnt mit der Deklaration einer Typstruktur, die die Struktur des Basistyps enthält. Zum Beispiel die Typstruktur für einen Subtyp des eingebauten Listen-Typs:
typedef struct {
PyListObject list;
int state;
} spamlistobject;
Beachten Sie, dass das Member der Basistypstruktur (hier PyListObject) das erste Member der Struktur sein muss; alle folgenden Member sind Ergänzungen. Beachten Sie auch, dass der Basistyp nicht über einen Zeiger referenziert wird; die tatsächlichen Inhalte seiner Struktur müssen einbezogen werden! (Das Ziel ist, dass das Speicherlayout des Anfangs der Subtypinstanz dasselbe ist wie das der Basistypinstanz.)
Als Nächstes muss der abgeleitete Typ ein Typobjekt deklarieren und initialisieren. Die meisten Slots im Typobjekt können auf Null initialisiert werden, was ein Signal ist, dass der entsprechende Basistyp-Slot hineinkopiert werden muss. Einige Slots, die richtig initialisiert werden müssen:
- Der Objekt-Header muss wie üblich gefüllt werden; der Typ sollte
&PyType_Typesein. - Der
tp_basicsizeSlot muss auf die Größe der Subtyp-Instanzstruktur gesetzt werden (im obigen Beispiel:sizeof(spamlistobject)). - Der
tp_baseSlot muss auf die Adresse des Typobjekts des Basistyps gesetzt werden. - Wenn der abgeleitete Slot Zeiger-Member definiert, erfordert der
tp_deallocSlot besondere Aufmerksamkeit, siehe unten; andernfalls kann er auf Null gesetzt werden, um die Deallokationsfunktion des Basistyps zu erben. - Der
tp_flagsSlot muss auf den üblichenPy_TPFLAGS_DEFAULTWert gesetzt werden. - Der
tp_nameSlot muss gesetzt werden; es wird empfohlen, auchtp_doczu setzen (diese werden nicht vererbt).
Wenn der Subtyp keine zusätzlichen Struktur-Member definiert (er definiert nur neues Verhalten, keine neuen Daten), können die Slots tp_basicsize und tp_dealloc auf Null gesetzt bleiben.
Der tp_dealloc Slot des Subtyps verdient besondere Aufmerksamkeit. Wenn der abgeleitete Typ keine zusätzlichen Zeiger-Member definiert, die bei der Deallokation des Objekts DECREF-t oder freigegeben werden müssen, kann er auf Null gesetzt werden. Andernfalls muss die tp_dealloc() Funktion des Subtyps Py_XDECREF() für alle PyObject * Member und die korrekte Speicherfreigabefunktion für alle anderen Zeiger, die er besitzt, aufrufen und dann den tp_dealloc() Slot der Basisklasse aufrufen. Dieser Aufruf muss über die Typstruktur der Basisklasse erfolgen, zum Beispiel beim Ableiten vom Standardlisten-Typ:
PyList_Type.tp_dealloc(self);
Wenn der Subtyp einen anderen Allokations-Heap als der Basistyp verwenden möchte, muss der Subtyp sowohl die Slots tp_alloc() als auch tp_free() überschreiben. Diese werden von den tp_new() und tp_dealloc() Slots der Basisklasse aufgerufen, bzw.
Um die Initialisierung des Typs abzuschließen, muss PyType_InitDict() aufgerufen werden. Dies ersetzt die in der Subtyp-Struktur auf Null initialisierten Slots durch den Wert der entsprechenden Basistyp-Slots. (Es füllt auch tp_dict, das Dictionary des Typs, und führt verschiedene andere Initialisierungen durch, die für Typobjekte notwendig sind.)
Ein Subtyp ist erst nutzbar, nachdem PyType_InitDict() für ihn aufgerufen wurde; dies geschieht am besten während der Modulinitialisierung, vorausgesetzt, der Subtyp gehört zu einem Modul. Eine Alternative für Subtypen, die zum Python-Kern hinzugefügt werden (und die nicht in einem bestimmten Modul leben), wäre die Initialisierung des Subtyps in ihrer Konstruktorfunktion. Es ist erlaubt, PyType_InitDict() mehrmals aufzurufen; der zweite und weitere Aufrufe haben keine Auswirkung. Um unnötige Aufrufe zu vermeiden, kann eine Prüfung auf tp_dict==NULL erfolgen.
(Während der Initialisierung des Python-Interpreters werden einige Typen tatsächlich verwendet, bevor sie initialisiert werden. Solange die tatsächlich benötigten Slots initialisiert sind, insbesondere tp_dealloc, funktioniert dies, ist aber fragil und wird nicht als allgemeine Praxis empfohlen.)
Um eine Subtypinstanz zu erstellen, wird der tp_new() Slot des Subtyps aufgerufen. Dieser sollte zuerst den tp_new() Slot des Basistyps aufrufen und dann die zusätzlichen Datenmember des Subtyps initialisieren. Zur weiteren Initialisierung der Instanz wird typischerweise der tp_init() Slot aufgerufen. Beachten Sie, dass der tp_new() Slot den tp_init() Slot **nicht** aufrufen sollte; dies obliegt dem Aufrufer von tp_new() (typischerweise einer Factory-Funktion). Es gibt Umstände, unter denen es angebracht ist, tp_init() nicht aufzurufen.
Wenn ein Subtyp einen tp_init() Slot definiert, sollte der tp_init() Slot normalerweise zuerst den tp_init() Slot des Basistyps aufrufen.
(XXX Hier sollte ein Absatz oder zwei über die Argumentübergabe stehen.)
Subtypisierung in Python
Der nächste Schritt ist, die Subtypisierung ausgewählter eingebauter Typen über eine Klassenerklärung in Python zu ermöglichen. Wenn wir uns vorerst auf einfache Vererbung beschränken, hier ist, was für eine einfache Klassenerklärung passiert
class C(B):
var1 = 1
def method1(self): pass
# etc.
Der Körper der Klassenerklärung wird in einer frischen Umgebung ausgeführt (grundsätzlich ein neues Dictionary als lokaler Namensraum), und dann wird C erstellt. Das Folgende erklärt, wie C erstellt wird.
Angenommen, B ist ein Typobjekt. Da Typobjekte Objekte sind und jedes Objekt einen Typ hat, hat B einen Typ. Da B selbst ein Typ ist, nennen wir seinen Typ auch seine Metatyp. B's Metatyp ist über type(B) oder B.__class__ zugänglich (letztere Notation ist neu für Typen; sie wurde in PEP 252 eingeführt). Nehmen wir an, dieser Metatyp ist M (für Metatyp). Die Klassenerklärung wird einen neuen Typ, C, erstellen. Da C genau wie B ein Typobjekt sein wird, betrachten wir die Erstellung von C als eine Instanziierung des Metatyps, M. Die für die Erstellung einer Unterklasse zu liefernden Informationen sind
- ihr Name (in diesem Beispiel der String „C“);
- ihre Basen (ein Singleton-Tupel, das B enthält);
- die Ergebnisse der Ausführung des Klassenkörpers in Form eines Dictionaries (z. B.
{"var1": 1, "method1": <functionmethod1 at ...>, ...}).
Die Klassenerklärung führt zum folgenden Aufruf
C = M("C", (B,), dict)
wobei dict das Dictionary ist, das aus der Ausführung des Klassenkörpers resultiert. Mit anderen Worten, der Metatyp (M) wird aufgerufen.
Beachten Sie, dass wir, obwohl das Beispiel nur eine Basis hat, immer noch eine (Singleton-)Sequenz von Basen übergeben; dies macht die Schnittstelle einheitlich mit dem Mehrfachvererbungsfall.
Im aktuellen Python wird dies der „Don Beaudry Hook“ nach seinem Erfinder genannt; es ist ein Ausnahmefall, der nur aufgerufen wird, wenn eine Basisklasse keine reguläre Klasse ist. Für eine reguläre Basisklasse (oder wenn keine Basisklasse angegeben ist) ruft das aktuelle Python PyClass_New(), die C-seitige Factory-Funktion für Klassen, direkt auf.
Unter dem neuen System wird dies geändert, sodass Python **immer** einen Metatyp ermittelt und ihn wie oben angegeben aufruft. Wenn ein oder mehrere Basen angegeben sind, wird der Typ der ersten Basis als Metatyp verwendet; wenn keine Basis angegeben ist, wird ein Standardmetatyp gewählt. Durch die Einstellung des Standardmetatyps auf PyClass_Type, dem Metatyp von „klassischen“ Klassen, wird das klassische Verhalten der Klassenerklärung beibehalten. Dieser Standard kann pro Modul geändert werden, indem die globale Variable __metaclass__ gesetzt wird.
Es gibt hier zwei weitere Verfeinerungen. Erstens ist es eine nützliche Funktion, einen Metatyp direkt angeben zu können. Wenn die Klassensuite eine Variable __metaclass__ definiert, ist dies der Metatyp, der aufgerufen werden soll. (Beachten Sie, dass das Setzen von __metaclass__ auf Modulebene nur Klassenerklärungen ohne Basisklasse und ohne explizite __metaclass__ Deklaration beeinflusst; aber das Setzen von __metaclass__ in einer Klassensuite überschreibt den Standardmetatyp bedingungslos.)
Zweitens müssen bei mehreren Basen nicht alle Basen den gleichen Metatyp haben. Dies wird als Metatyp-Konflikt bezeichnet [1]. Einige Metatyp-Konflikte können durch Suchen im Satz von Basen nach einem Metatyp, der von allen anderen gegebenen Metatypen abgeleitet ist, gelöst werden. Wenn ein solcher Metatyp nicht gefunden werden kann, wird eine Ausnahme ausgelöst und die Klassenerklärung schlägt fehl.
Diese Konfliktlösung kann durch die Metatyp-Konstruktoren implementiert werden: Die Klassenerklärung ruft einfach den Metatyp der ersten Basis auf (oder den, der durch die Variable __metaclass__ angegeben ist), und der Konstruktor dieses Metatyps sucht nach dem am weitesten abgeleiteten Metatyp. Wenn dieser es selbst ist, wird fortgefahren; andernfalls ruft er den Konstruktor dieses Metatyps auf. (Ultimative Flexibilität: Ein anderer Metatyp könnte wählen, dass alle Basen den gleichen Metatyp haben müssen, oder dass es nur eine Basisklasse gibt, oder was auch immer.)
(In [1] wird automatisch ein neuer Metatyp abgeleitet, der eine Unterklasse aller gegebenen Metatypen ist. Da es in Python jedoch fraglich ist, wie widersprüchliche Methodendefinitionen der verschiedenen Metatypen zusammengeführt werden sollen, glaube ich nicht, dass dies machbar ist. Sollte sich der Bedarf ergeben, kann der Benutzer einen solchen Metatyp manuell ableiten und ihn über die Variable __metaclass__ angeben. Es ist auch möglich, einen neuen Metatyp zu haben, der dies tut.)
Beachten Sie, dass der Aufruf von M erfordert, dass M selbst einen Typ hat: den Meta-Metatyp. Und der Meta-Metatyp hat einen Typ, den Meta-Meta-Metatyp. Und so weiter. Dies wird normalerweise auf einer bestimmten Ebene abgekürzt, indem ein Metatyp sein eigener Metatyp ist. Dies geschieht tatsächlich in Python: Die ob_type Referenz in PyType_Type ist auf &PyType_Type gesetzt. In Abwesenheit von Drittanbieter-Metatypen ist PyType_Type der einzige Metatyp im Python-Interpreter.
(In einer früheren Version dieses PEP gab es eine zusätzliche Meta-Ebene, und es gab einen Meta-Metatyp namens „turtle“. Dies erwies sich als unnötig.)
In jedem Fall wird die Arbeit zur Erstellung von C durch den tp_new() Slot von M erledigt. Er alloziert Speicher für eine „erweiterte“ Typstruktur, die Folgendes enthält: das Typobjekt; die Hilfsstrukturen (as_sequence etc.); das String-Objekt, das den Typnamen enthält (um sicherzustellen, dass dieses Objekt nicht deallokiert wird, während das Typobjekt es noch referenziert); und etwas Hilfsspeicher (der später beschrieben wird). Er initialisiert diesen Speicher mit Nullen, mit Ausnahme einiger kritischer Slots (z. B. wird tp_name auf den Typnamen gesetzt) und setzt dann den tp_base Slot, um auf B zu zeigen. Dann wird PyType_InitDict() aufgerufen, um die Slots von B zu erben. Schließlich wird der tp_dict Slot von C mit dem Inhalt des Namensraum-Dictionaries (dem dritten Argument des Aufrufs von M) aktualisiert.
Mehrfachvererbung
Die Python-Klassenerklärung unterstützt Mehrfachvererbung, und wir werden auch Mehrfachvererbung mit eingebauten Typen unterstützen.
Es gibt jedoch einige Einschränkungen. Die C-Laufzeitarchitektur macht es nicht praktikabel, einen sinnvollen Subtyp von zwei verschiedenen eingebauten Typen zu haben, außer in einigen degenerierten Fällen. Die Änderung der C-Laufzeit zur Unterstützung einer vollständig allgemeinen Mehrfachvererbung wäre zu viel Umwälzung der Codebasis.
Das Hauptproblem bei Mehrfachvererbung von verschiedenen eingebauten Typen ergibt sich aus der Tatsache, dass die C-Implementierung von eingebauten Typen direkt auf Strukturmember zugreift; der C-Compiler generiert einen Offset relativ zum Objektzeiger, und das war's. Zum Beispiel deklarieren die Typstrukturen von Listen und Dictionaries jeweils eine Reihe unterschiedlicher, aber überlappender Strukturmember. Eine C-Funktion, die auf ein Objekt zugreift, das eine Liste erwartet, funktioniert nicht, wenn sie mit einem Dictionary übergeben wird, und umgekehrt, und es gibt nicht viel, was wir dagegen tun könnten, ohne allen Code umzuschreiben, der auf Listen und Dictionaries zugreift. Dies wäre zu viel Arbeit, also werden wir das nicht tun.
Das Problem bei Mehrfachvererbung wird durch widersprüchliche Strukturmember-Allokationen verursacht. In Python definierte Klassen speichern ihre Instanzvariablen normalerweise nicht in Strukturmembern: sie werden in einem Instanz-Dictionary gespeichert. Dies ist der Schlüssel zu einer Teillösung. Angenommen, wir haben die folgenden beiden Klassen
class A(dictionary):
def foo(self): pass
class B(dictionary):
def bar(self): pass
class C(A, B): pass
(Hier ist „dictionary“ der Typ von eingebauten Dictionary-Objekten, auch bekannt als type({}) oder {}.__class__ oder types.DictType.) Wenn wir uns das Struktur-Layout ansehen, stellen wir fest, dass eine A-Instanz das Layout eines Dictionaries gefolgt vom __dict__ Zeiger hat, und eine B-Instanz hat das gleiche Layout; da es keine Konflikte beim Strukturmember-Layout gibt, ist dies in Ordnung.
Hier ist ein weiteres Beispiel
class X(object):
def foo(self): pass
class Y(dictionary):
def bar(self): pass
class Z(X, Y): pass
(Hier ist „object“ die Basis für alle eingebauten Typen; ihr Struktur-Layout enthält nur die ob_refcnt und ob_type Member.) Dieses Beispiel ist komplizierter, da der __dict__ Zeiger für X-Instanzen einen anderen Offset hat als für Y-Instanzen. Wo ist der __dict__ Zeiger für Z-Instanzen? Die Antwort ist, dass der Offset für den __dict__ Zeiger nicht hartcodiert ist, er ist im Typobjekt gespeichert.
Angenommen, auf einem bestimmten Rechner ist eine „object“-Struktur 8 Bytes lang, und eine „dictionary“-Struktur ist 60 Bytes, und ein Objektzeiger ist 4 Bytes. Dann ist eine X-Struktur 12 Bytes (eine Objektstruktur gefolgt von einem __dict__ Zeiger), und eine Y-Struktur ist 64 Bytes (eine Dictionary-Struktur gefolgt von einem __dict__ Zeiger). Die Z-Struktur hat in diesem Beispiel das gleiche Layout wie die Y-Struktur. Jedes Typobjekt (X, Y und Z) hat einen „__dict__ offset“, der verwendet wird, um den __dict__ Zeiger zu finden. Das Rezept zum Nachschlagen einer Instanzvariable lautet also
- hole den Typ der Instanz
- hole den
__dict__Offset aus dem Typobjekt - addiere den
__dict__Offset zum Instanzzeiger - suche in der resultierenden Adresse nach einer Dictionary-Referenz
- suche den Instanzvariablennamen in diesem Dictionary
Natürlich kann dieses Rezept nur in C implementiert werden, und ich habe einige Details weggelassen. Aber dies ermöglicht uns die Verwendung von Mehrfachvererbungsmustern ähnlich denen, die wir mit klassischen Klassen verwenden können.
XXX Ich sollte hier den vollständigen Algorithmus zur Bestimmung der Kompatibilität von Basisklassen aufschreiben, aber ich habe im Moment keine Lust dazu. Sehen Sie sich best_base() in typeobject.c in der unten erwähnten Implementierung an.
MRO: Method Resolution Order (die Lookup-Regel)
Mit Mehrfachvererbung stellt sich die Frage nach der Method Resolution Order (MRO): die Reihenfolge, in der eine Klasse oder ein Typ und seine Basen durchsucht werden, um eine Methode eines bestimmten Namens zu finden.
Im klassischen Python wird die Regel durch die folgende rekursive Funktion gegeben, auch bekannt als die links-nach-rechts-Tiefensuche-Regel
def classic_lookup(cls, name):
if cls.__dict__.has_key(name):
return cls.__dict__[name]
for base in cls.__bases__:
try:
return classic_lookup(base, name)
except AttributeError:
pass
raise AttributeError, name
Das Problem damit wird offensichtlich, wenn wir ein „Diamantdiagramm“ betrachten
class A:
^ ^ def save(self): ...
/ \
/ \
/ \
/ \
class B class C:
^ ^ def save(self): ...
\ /
\ /
\ /
\ /
class D
Pfeile zeigen von einem Subtyp zu seinem Basis-Typ type(s). Dieses spezielle Diagramm bedeutet, dass B und C von A abgeleitet sind und D von B und C (und damit auch indirekt von A) abgeleitet ist.
Angenommen, C überschreibt die Methode save(), die in der Basis A definiert ist. (C.save() ruft wahrscheinlich A.save() auf und speichert dann seinen eigenen Zustand.) B und D überschreiben save() nicht. Wenn wir save() auf einer D-Instanz aufrufen, welche Methode wird aufgerufen? Gemäß der klassischen Suchregel wird A.save() aufgerufen, wobei C.save() ignoriert wird!
Das ist nicht gut. Es bricht wahrscheinlich C (sein Zustand wird nicht gespeichert), was den gesamten Zweck der Vererbung von C zunichte macht.
Warum war das im klassischen Python kein Problem? Diamantdiagramme sind in klassischen Python-Klassenhierarchien selten zu finden. Die meisten Klassenhierarchien verwenden einfache Vererbung, und Mehrfachvererbung beschränkt sich normalerweise auf Mix-in-Klassen. Tatsächlich ist das hier gezeigte Problem wahrscheinlich der Grund, warum Mehrfachvererbung im klassischen Python unbeliebt ist.
Warum wird dies im neuen System ein Problem sein? Der „object“-Typ an der Spitze der Typenhierarchie definiert eine Reihe von Methoden, die von Subtypen sinnvoll erweitert werden können, z. B. __getattr__().
(Zwischenbemerkung: Im klassischen Python ist die Methode __getattr__() nicht wirklich die Implementierung für die Attributabfrage; es ist ein Haken, der nur aufgerufen wird, wenn ein Attribut nicht auf normalem Wege gefunden werden kann. Dies wurde oft als Mangel angeführt – einige Klassendesigns haben einen legitimen Bedarf an einer Methode __getattr__(), die für **alle** Attributreferenzen aufgerufen wird. Aber dann muss diese Methode natürlich die Standardimplementierung direkt aufrufen können. Der natürlichste Weg ist, die Standardimplementierung als object.__getattr__(self, name) verfügbar zu machen.)
Somit wird eine klassische Klassenhierarchie wie diese
class B class C:
^ ^ def __getattr__(self, name): ...
\ /
\ /
\ /
\ /
class D
wird im neuen System zu einem Diamantdiagramm
object:
^ ^ __getattr__()
/ \
/ \
/ \
/ \
class B class C:
^ ^ def __getattr__(self, name): ...
\ /
\ /
\ /
\ /
class D
und während im ursprünglichen Diagramm C.__getattr__() aufgerufen wird, würde im neuen System mit der klassischen Suchregel object.__getattr__() aufgerufen werden!
Glücklicherweise gibt es eine Suchregel, die besser ist. Sie ist etwas schwierig zu erklären, aber sie tut das Richtige im Diamantdiagramm und sie ist identisch mit der klassischen Suchregel, wenn keine Diamanten im Vererbungsdiagramm vorhanden sind (wenn es ein Baum ist).
Die neue Suchregel konstruiert eine Liste aller Klassen im Vererbungsdiagramm in der Reihenfolge, in der sie durchsucht werden. Diese Konstruktion wird zur Zeit der Klassendefinition durchgeführt, um Zeit zu sparen. Um die neue Suchregel zu erklären, betrachten wir zunächst, wie eine solche Liste für die klassische Suchregel aussehen würde. Beachten Sie, dass im Falle von Diamanten die klassische Suche einige Klassen mehrmals besucht. Zum Beispiel, im obigen ABCD-Diamantdiagramm, besucht die klassische Suchregel die Klassen in dieser Reihenfolge
D, B, A, C, A
Beachten Sie, wie A zweimal in der Liste vorkommt. Der zweite Eintrag ist redundant, da alles, was dort gefunden werden könnte, bereits beim Durchsuchen des ersten Eintrags gefunden worden wäre.
Wir nutzen diese Beobachtung, um unsere neue Suchregel zu erklären. Verwenden Sie die klassische Suchregel, konstruieren Sie die Liste der Klassen, die durchsucht würden, einschließlich Duplikaten. Nun entfernen Sie für jede Klasse, die mehrmals in der Liste vorkommt, alle Vorkommen außer dem letzten. Die resultierende Liste enthält jede Ahnenklasse genau einmal (einschließlich der am weitesten abgeleiteten Klasse, D im Beispiel).
Die Suche nach Methoden in dieser Reihenfolge wird im Diamantdiagramm das Richtige tun. Aufgrund der Art und Weise, wie die Liste konstruiert wird, ändert sie die Suchreihenfolge nicht in Situationen, in denen kein Diamant beteiligt ist.
Ist das rückwärts inkompatibel? Wird es bestehenden Code brechen? Es würde, wenn wir die Method Resolution Order für alle Klassen ändern würden. In Python 2.2 wird die neue Suchregel jedoch nur auf Typen angewendet, die von eingebauten Typen abgeleitet sind, was eine neue Funktion ist. Klassenerklärungen ohne Basisklasse erstellen „klassische Klassen“, und das Gleiche gilt für Klassenerklärungen, deren Basisklassen selbst klassische Klassen sind. Für klassische Klassen wird die klassische Suchregel verwendet. (Um mit der neuen Suchregel für klassische Klassen zu experimentieren, können Sie explizit einen anderen Metatyp angeben.) Wir werden auch ein Werkzeug bereitstellen, das eine Klassenhierarchie analysiert, um nach Methoden zu suchen, die von einer Änderung der Method Resolution Order betroffen wären.
XXX Eine andere Möglichkeit, die Motivation für die neue MRO zu erklären, von Damian Conway: Man verwendet nie die Methode, die in einer Basisklasse definiert ist, wenn sie in einer abgeleiteten Klasse definiert ist, die man noch nicht durchlaufen hat (mit der alten Suchreihenfolge).
XXX Zu erledigen
Zusätzliche Themen, die in diesem PEP diskutiert werden sollen
- Rückwärtskompatibilitätsprobleme!!!
- Klassenmethoden und statische Methoden
- kooperative Methoden und
super() - Mapping zwischen Typobjekt-Slots (tp_foo) und speziellen Methoden (
__foo__) (tatsächlich gehört dies möglicherweise zu PEP 252) - Eingebaute Namen für eingebaute Typen (object, int, str, list etc.)
__dict__und__dictoffset____slots__- Das
HEAPTYPEFlag-Bit - GC-Unterstützung
- API-Dokumentation für alle neuen Funktionen
- wie man
__new__benutzt - Schreiben von Metaklassen (mit
mro()etc.) - High-Level-Benutzerübersicht
Offene Fragen
- brauchen wir
__del__? - Zuweisung zu
__dict__,__bases__ - inkonsistente Benennung (z. B. tp_dealloc/tp_new/tp_init/tp_alloc/tp_free)
- Alias für das eingebaute ‚dict‘ für ‚dictionary‘ hinzufügen?
- wenn Dictionaries/Listen-Subklassen etc. an Systemfunktionen übergeben werden, werden die
__getitem__Überschreibungen (etc.) nicht immer verwendet
Implementierung
Eine Prototyp-Implementierung dieses PEP (und für PEP 252) ist von CVS und in der Serie der Python 2.2 Alpha- und Beta-Releases verfügbar. Einige Beispiele für die hier beschriebenen Funktionen finden Sie in der Datei Lib/test/test_descr.py und im Erweiterungsmodul Modules/xxsubtype.c.
Referenzen
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0253.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT