PEP 252 – Typen wie Klassen aussehen lassen
- Autor:
- Guido van Rossum <guido at python.org>
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 19. April 2001
- Python-Version:
- 2.2
- Post-History:
Inhaltsverzeichnis
Zusammenfassung
Diese PEP schlägt Änderungen an der Introspektions-API für Typen vor, die sie wie Klassen aussehen lassen und ihre Instanzen wie Klasseninstanzen. Zum Beispiel wird type(x) für die meisten integrierten Typen äquivalent zu x.__class__ sein. Wenn C x.__class__ ist, wird x.meth(a) im Allgemeinen äquivalent zu C.meth(x, a) sein, und C.__dict__ enthält x's Methoden und andere Attribute.
Diese PEP führt auch einen neuen Ansatz zur Spezifizierung von Attributen ein, der Attributdeskriptoren oder kurz Deskriptoren verwendet. Deskriptoren vereinheitlichen und verallgemeinern mehrere verschiedene gängige Mechanismen zur Beschreibung von Attributen: Ein Deskriptor kann eine Methode, ein typisiertes Feld in der Objektstruktur oder ein verallgemeinertes Attribut beschreiben, das durch Getter- und Setter-Funktionen dargestellt wird.
Basierend auf der verallgemeinerten Deskriptor-API führt diese PEP auch eine Möglichkeit ein, Klassen- und statische Methoden zu deklarieren.
[Editorischer Hinweis: Die in dieser PEP beschriebenen Ideen wurden in Python integriert. Die PEP beschreibt die Implementierung nicht mehr genau.]
Einleitung
Einer der ältesten Sprachfehler von Python ist der Unterschied zwischen Klassen und Typen. Zum Beispiel können Sie den Dictionary-Typ nicht direkt unterklassifizieren, und die Introspektionsschnittstelle zum Ermitteln von Methoden und Instanzvariablen eines Objekts ist für Typen und Klassen unterschiedlich.
Die Behebung der Klassen-/Typen-Trennung ist eine große Anstrengung, da sie viele Aspekte der Python-Implementierung betrifft. Diese PEP befasst sich damit, die Introspektions-API für Typen genauso aussehen zu lassen wie die für Klassen. Andere PEPs werden vorschlagen, Klassen wie Typen aussehen zu lassen und von integrierten Typen zu unterklassifizieren; diese Themen sind für diese PEP nicht relevant.
Introspektions-APIs
Introspektion befasst sich mit der Ermittlung, welche Attribute ein Objekt hat. Die sehr allgemeine getattr/setattr-API von Python macht es unmöglich zu garantieren, dass es immer eine Möglichkeit gibt, eine Liste aller Attribute zu erhalten, die von einem bestimmten Objekt unterstützt werden, aber in der Praxis haben sich zwei Konventionen herausgebildet, die zusammen für fast alle Objekte funktionieren. Ich werde sie die klassenbasierte Introspektions-API und die typbasierte Introspektions-API nennen; kurz Klassen-API und Typ-API.
Die klassenbasierte Introspektions-API wird hauptsächlich für Klasseninstanzen verwendet; sie wird auch von Jim Fultons ExtensionClasses verwendet. Sie geht davon aus, dass alle Datenattribute eines Objekts x im Dictionary x.__dict__ gespeichert sind und dass alle Methoden und Klassenvariablen durch Inspektion der Klasse von x, geschrieben als x.__class__, gefunden werden können. Klassen haben ein __dict__-Attribut, das ein Dictionary mit Methoden und Klassenvariablen liefert, die von der Klasse selbst definiert wurden, und ein __bases__-Attribut, das ein Tupel von Basisklassen ist, die rekursiv inspiziert werden müssen. Einige Annahmen hier sind:
- Attribute, die im Instanz-Dictionary definiert sind, überschreiben Attribute, die von der Klasse des Objekts definiert wurden.
- Attribute, die in einer abgeleiteten Klasse definiert sind, überschreiben Attribute, die in einer Basisklasse definiert sind.
- Attribute in einer früheren Basisklasse (d. h. weiter vorne in
__bases__) überschreiben Attribute in einer späteren Basisklasse.
(Die letzten beiden Regeln werden oft als Tiefensuche von links nach rechts für die Attributsuche zusammengefasst. Dies ist die klassische Python-Regel für die Attributauflösung. Beachten Sie, dass PEP 253 vorschlagen wird, die Reihenfolge der Attributauflösung zu ändern, und wenn dies akzeptiert wird, wird diese PEP folgen.)
Die typbasierte Introspektions-API wird in der einen oder anderen Form von den meisten integrierten Objekten unterstützt. Sie verwendet zwei spezielle Attribute, __members__ und __methods__. Das Attribut __methods__, falls vorhanden, ist eine Liste von Methodennamen, die vom Objekt unterstützt werden. Das Attribut __members__, falls vorhanden, ist eine Liste von Datenattributnamen, die vom Objekt unterstützt werden.
Die Typ-API wird manchmal mit einem __dict__ kombiniert, das dasselbe tut wie für Instanzen (z. B. für Funktions-Objekte in Python 2.1 enthält f.__dict__ die dynamischen Attribute von f, während f.__members__ die Namen der statisch definierten Attribute von f auflistet).
Einige Vorsichtsmaßnahmen sind geboten: Einige Objekte listen ihre "intrinsischen" Attribute (wie __dict__ und __doc__) nicht in __members__ auf, während andere dies tun; manchmal kommen Attributnamen sowohl in __members__ oder __methods__ als auch als Schlüssel in __dict__ vor, in welchem Fall es jedermanns Vermutung ist, ob der in __dict__ gefundene Wert verwendet wird oder nicht.
Die Typ-API wurde nie sorgfältig spezifiziert. Sie ist Teil der Python-Folklore, und die meisten Drittanbietererweiterungen unterstützen sie, weil sie Beispielen folgen, die sie unterstützen. Außerdem unterstützt jeder Typ, der Py_FindMethod() und/oder PyMember_Get() in seinem tp_getattr-Handler verwendet, sie, weil diese beiden Funktionen die Attributnamen __methods__ und __members__ speziell behandeln.
Jim Fultons ExtensionClasses ignorieren die Typ-API und emulieren stattdessen die Klassen-API, die leistungsfähiger ist. In dieser PEP schlage ich vor, die Typ-API zugunsten der Unterstützung der Klassen-API für alle Typen auslaufen zu lassen.
Ein Argument für die Klassen-API ist, dass sie keine Instanzerstellung erfordert, um herauszufinden, welche Attribute ein Typ unterstützt; dies ist wiederum nützlich für Dokumentationsprozessoren. Zum Beispiel exportiert das Socket-Modul das SocketType-Objekt, aber dieses sagt uns derzeit nicht, welche Methoden auf Socket-Objekten definiert sind. Mit der Klassen-API würde SocketType genau zeigen, welche Methoden für Socket-Objekte existieren, und wir könnten sogar ihre Docstrings extrahieren, ohne einen Socket zu erstellen. (Da dies ein C-Erweiterungsmodul ist, ist der Ansatz des Quellcode-Scannens zur Extraktion von Docstrings in diesem Fall nicht praktikabel.)
Spezifikation der klassenbasierten Introspektions-API
Objekte können zwei Arten von Attributen haben: statische und dynamische. Die Namen und manchmal auch andere Eigenschaften von statischen Attributen sind durch Inspektion des Typs oder der Klasse des Objekts erkennbar, die über obj.__class__ oder type(obj) zugänglich ist. (Ich verwende Typ und Klasse austauschbar; ein ungeschickter, aber beschreibender Begriff, der für beide passt, ist "Meta-Objekt".)
(XXX statisch und dynamisch sind keine guten Begriffe, da "statische" Attribute tatsächlich recht dynamisch sein können und nichts mit statischen Klassenmitgliedern in C++ oder Java zu tun haben. Barry schlägt vor, stattdessen unveränderlich und veränderlich zu verwenden, aber diese Wörter haben bereits präzise und unterschiedliche Bedeutungen in leicht unterschiedlichen Kontexten, daher denke ich, dass dies immer noch verwirrend wäre.)
Beispiele für dynamische Attribute sind Instanzvariablen von Klasseninstanzen, Modulattribute usw. Beispiele für statische Attribute sind die Methoden von integrierten Objekten wie Listen und Dictionaries sowie die Attribute von Frame- und Code-Objekten (f.f_code, c.co_filename usw.). Wenn ein Objekt mit dynamischen Attributen diese über sein __dict__-Attribut preisgibt, ist __dict__ ein statisches Attribut.
Die Namen und Werte dynamischer Eigenschaften werden typischerweise in einem Dictionary gespeichert, und dieses Dictionary ist typischerweise als obj.__dict__ zugänglich. Der Rest dieser Spezifikation befasst sich mehr mit der Ermittlung der Namen und Eigenschaften statischer Attribute als mit dynamischen Attributen; letztere sind durch Inspektion von obj.__dict__ leicht zu entdecken.
In der folgenden Diskussion unterscheide ich zwei Arten von Objekten: reguläre Objekte (wie Listen, Ganzzahlen, Funktionen) und Meta-Objekte. Typen und Klassen sind Meta-Objekte. Meta-Objekte sind auch reguläre Objekte, aber wir interessieren uns hauptsächlich für sie, weil sie durch das __class__-Attribut von regulären Objekten (oder durch das __bases__-Attribut anderer Meta-Objekte) referenziert werden.
Die Klassen-Introspektions-API besteht aus den folgenden Elementen:
- die Attribute
__class__und__dict__auf regulären Objekten; - die Attribute
__bases__und__dict__auf Meta-Objekten; - Vorrangregeln;
- Attributdeskriptoren.
Zusammen sagen diese nicht nur etwas über **alle** Attribute aus, die von einem Meta-Objekt definiert werden, sondern sie helfen uns auch, den Wert eines bestimmten Attributs eines gegebenen Objekts zu berechnen.
- Das
__dict__-Attribut bei regulären ObjektenEin reguläres Objekt kann ein
__dict__-Attribut haben. Wenn ja, sollte dies eine Abbildung (nicht unbedingt ein Dictionary) sein, die mindestens__getitem__(),keys()undhas_key()unterstützt. Dies liefert die dynamischen Attribute des Objekts. Die Schlüssel in der Abbildung ergeben die Attributnamen und die entsprechenden Werte ergeben ihre Werte.Typischerweise ist der Wert eines Attributs mit einem bestimmten Namen dasselbe Objekt wie der Wert, der diesem Namen als Schlüssel im
__dict__entspricht. Mit anderen Worten,obj.__dict__['spam']istobj.spam. (Aber siehe die Vorrangregeln unten; ein statisches Attribut mit demselben Namen **kann** den Dictionary-Eintrag überschreiben.) - Das
__class__-Attribut bei regulären ObjektenEin reguläres Objekt hat normalerweise ein
__class__-Attribut. Wenn ja, verweist es auf ein Meta-Objekt. Ein Meta-Objekt kann statische Attribute für das reguläre Objekt definieren, dessen__class__es ist. Dies geschieht normalerweise durch den folgenden Mechanismus: - Das
__dict__-Attribut bei Meta-ObjektenEin Meta-Objekt kann ein
__dict__-Attribut haben, in der gleichen Form wie das__dict__-Attribut für reguläre Objekte (eine Abbildung, aber nicht unbedingt ein Dictionary). Wenn ja, sind die Schlüssel des__dict__des Meta-Objekts Namen von statischen Attributen für das entsprechende reguläre Objekt. Die Werte sind Attributdeskriptoren; diese werden wir später erklären. Eine ungebundene Methode ist ein Sonderfall eines Attributdeskriptors.Da ein Meta-Objekt auch ein reguläres Objekt ist, entsprechen die Einträge im
__dict__eines Meta-Objekts Attributen des Meta-Objekts; es können jedoch Transformationen angewendet werden, und Basisklassen (siehe unten) können zusätzliche dynamische Attribute definieren. Mit anderen Worten,mobj.spamist nicht immermobj.__dict__['spam']. (Diese Regel enthält eine Ausnahmeregelung, denn bei Klassen, wennC.__dict__['spam']eine Funktion ist, istC.spamein ungebundenes Methodenobjekt.) - Das
__bases__-Attribut bei Meta-ObjektenEin Meta-Objekt kann ein
__bases__-Attribut haben. Wenn ja, sollte dies eine Sequenz (nicht unbedingt ein Tupel) anderer Meta-Objekte, der Basisklassen, sein. Ein fehlendes__bases__ist äquivalent zu einer leeren Basisklassen-Sequenz. Es darf niemals einen Zyklus in der Beziehung zwischen Meta-Objekten geben, die durch__bases__-Attribute definiert sind; mit anderen Worten, die__bases__-Attribute definieren einen gerichteten azyklischen Graphen mit Bögen, die von abgeleiteten Meta-Objekten zu ihren Basis-Meta-Objekten zeigen. (Es ist nicht notwendigerweise ein Baum, da mehrere Klassen dieselbe Basisklasse haben können.) Die__dict__-Attribute eines Meta-Objekts im Vererbungsgraphen liefern Attributdeskriptoren für das reguläre Objekt, dessen__class__-Attribut auf die Wurzel des Vererbungstyps zeigt (was nicht dasselbe ist wie die Wurzel der Vererbungshierarchie – eher das Gegenteil, am unteren Ende, wie Vererbungstypen typischerweise gezeichnet werden). Deskriptoren werden zuerst im Dictionary des Wurzel-Meta-Objekts gesucht, dann in seinen Basisklassen, gemäß einer Vorrangregel (siehe nächster Absatz). - Vorrangregeln
Wenn zwei Meta-Objekte im Vererbungsgraphen für ein gegebenes reguläres Objekt einen Attributdeskriptor mit demselben Namen definieren, liegt die Suchreihenfolge beim Meta-Objekt. Dies ermöglicht es verschiedenen Meta-Objekten, unterschiedliche Suchreihenfolgen zu definieren. Insbesondere verwenden klassische Klassen die alte Tiefensuche von links nach rechts, während neue Klassen eine fortgeschrittenere Regel verwenden (siehe Abschnitt über Method Resolution Order in PEP 253).
Wenn ein dynamisches Attribut (eines, das im
__dict__eines regulären Objekts definiert ist) denselben Namen wie ein statisches Attribut hat (eines, das von einem Meta-Objekt im Vererbungsgraphen, der an das__class__des regulären Objekts gekoppelt ist, definiert wurde), hat das statische Attribut Vorrang, wenn es ein Deskriptor ist, der eine__set__-Methode definiert (siehe unten); andernfalls (wenn keine__set__-Methode vorhanden ist) hat das dynamische Attribut Vorrang. Mit anderen Worten, für Datenattribute (die mit einer__set__-Methode) überschreibt die statische Definition die dynamische Definition, aber für andere Attribute überschreibt dynamisch statisch.Begründung: Wir können keine einfache Regel wie "statisch überschreibt dynamisch" oder "dynamisch überschreibt statisch" haben, da einige statische Attribute tatsächlich dynamische Attribute überschreiben; zum Beispiel wird ein Schlüssel '__class__' im
__dict__einer Instanz zugunsten des statisch definierten__class__-Zeigers ignoriert, aber andererseits überschreiben die meisten Schlüssel ininst.__dict__Attribute, die ininst.__class__definiert sind. Das Vorhandensein einer__set__-Methode auf einem Deskriptor zeigt an, dass dies ein Daten-Deskriptor ist. (Selbst schreibgeschützte Daten-Deskriptoren haben eine__set__-Methode: sie löst immer eine Ausnahme aus.) Das Fehlen einer__set__-Methode auf einem Deskriptor zeigt an, dass der Deskriptor kein Interesse daran hat, Zuweisungen abzufangen, und dann gilt die klassische Regel: eine Instanzvariable mit demselben Namen wie eine Methode verdeckt die Methode, bis sie gelöscht wird. - Attributdeskriptoren
Hier wird es interessant – und unübersichtlich. Attributdeskriptoren (kurz Deskriptoren) werden im
__dict__des Meta-Objekts (oder im__dict__eines seiner Vorfahren) gespeichert und haben zwei Verwendungszwecke: Ein Deskriptor kann verwendet werden, um den entsprechenden Attributwert vom (regulären, nicht-Meta-)Objekt abzurufen oder zu setzen, und er hat eine zusätzliche Schnittstelle, die das Attribut für Dokumentations- und Introspektionszwecke beschreibt.Es gibt wenig Vorarbeit in Python für das Design der Schnittstelle eines Deskriptors, weder für das Abrufen/Setzen des Werts noch für die Beschreibung des Attributs. Lediglich einige triviale Eigenschaften (es ist vernünftig anzunehmen, dass
__name__und__doc__der Name und die Docstring des Attributs sein sollten). Ich werde eine solche API unten vorschlagen.Wenn ein Objekt, das im
__dict__des Meta-Objekts gefunden wird, kein Attributdeskriptor ist, diktieren Abwärtskompatibilität bestimmte minimale Semantiken. Dies bedeutet im Grunde, dass, wenn es sich um eine Python-Funktion oder eine ungebundene Methode handelt, das Attribut eine Methode ist; andernfalls ist es der Standardwert für ein dynamisches Datenattribut. Abwärtskompatibilität diktiert auch, dass (in Abwesenheit einer__setattr__-Methode) die Zuweisung zu einem Attribut, das einer Methode entspricht, legal ist und dass dies ein Datenattribut erstellt, das die Methode für diese spezielle Instanz verdeckt. Diese Semantik ist jedoch nur für die Abwärtskompatibilität mit regulären Klassen erforderlich.
Die Introspektions-API ist eine schreibgeschützte API. Wir definieren nicht die Auswirkung von Zuweisungen an spezielle Attribute (__dict__, __class__ und __bases__) oder die Auswirkung von Zuweisungen an die Einträge eines __dict__. Im Allgemeinen sollten solche Zuweisungen als tabu betrachtet werden. Eine zukünftige PEP kann einige Semantiken für solche Zuweisungen definieren. (Insbesondere da Instanzen derzeit Zuweisungen zu __class__ und __dict__ unterstützen, und Klassen Zuweisungen zu __bases__ und __dict__ unterstützen.)
Spezifikation der Attribut-Deskriptor-API
Attributdeskriptoren können die folgenden Attribute haben. In den Beispielen ist x ein Objekt, C ist x.__class__, x.meth() ist eine Methode und x.ivar ist ein Datenattribut oder eine Instanzvariable. Alle Attribute sind optional – ein bestimmtes Attribut kann auf einem gegebenen Deskriptor vorhanden sein oder nicht. Ein fehlendes Attribut bedeutet, dass die entsprechende Information nicht verfügbar ist oder die entsprechende Funktionalität nicht implementiert ist.
__name__: der Attributname. Aufgrund von Aliasen und Umbenennungen kann das Attribut unter einem anderen Namen bekannt sein (zusätzlich oder ausschließlich), aber dies ist der Name, unter dem es geboren wurde. Beispiel:C.meth.__name__ == 'meth'.__doc__: der Dokumentationsstring des Attributs. Dies kann None sein.__objclass__: die Klasse, die dieses Attribut deklariert hat. Der Deskriptor gilt nur für Objekte, die Instanzen dieser Klasse sind (dies schließt Instanzen ihrer Unterklassen ein). Beispiel:C.meth.__objclass__ is C.__get__(): eine Funktion, die mit einem oder zwei Argumenten aufrufbar ist und den Attributwert von einem Objekt abruft. Dies wird auch als "Bindungs"-Operation bezeichnet, da sie ein "gebundenes Methoden"-Objekt im Falle von Methodendeskriptoren zurückgeben kann. Das erste Argument, X, ist das Objekt, von dem das Attribut abgerufen oder an das es gebunden werden muss. Wenn X None ist, sollte das optionale zweite Argument, T, ein Meta-Objekt sein, und die Bindungsoperation kann eine **ungebundene** Methode zurückgeben, die auf Instanzen von T beschränkt ist. Wenn sowohl X als auch T angegeben sind, sollte X eine Instanz von T sein. Genau das, was von der Bindungsoperation zurückgegeben wird, hängt von der Semantik des Deskriptors ab; zum Beispiel ignorieren statische Methoden und Klassenmethoden (siehe unten) die Instanz und binden sich an den Typ.__set__(): eine Funktion mit zwei Argumenten, die den Attributwert auf dem Objekt setzt. Wenn das Attribut schreibgeschützt ist, kann diese Methode eine TypeError- oderAttributeError-Ausnahme auslösen (beide sind erlaubt, da beide historisch für undefinierte oder nicht setzbare Attribute gefunden werden). Beispiel:C.ivar.set(x, y) ~~ x.ivar = y.
Statische und Klassenmethoden
Die Deskriptor-API ermöglicht die Hinzufügung von statischen und Klassenmethoden. Statische Methoden sind leicht zu beschreiben: Sie verhalten sich ziemlich wie statische Methoden in C++ oder Java. Hier ist ein Beispiel:
class C:
def foo(x, y):
print "staticmethod", x, y
foo = staticmethod(foo)
C.foo(1, 2)
c = C()
c.foo(1, 2)
Sowohl der Aufruf C.foo(1, 2) als auch der Aufruf c.foo(1, 2) rufen foo() mit zwei Argumenten auf und geben "staticmethod 1 2" aus. In der Definition von foo() ist kein "self" deklariert, und im Aufruf ist keine Instanz erforderlich.
Die Zeile "foo = staticmethod(foo)" in der Klassendefinition ist das entscheidende Element: Dies macht foo() zu einer statischen Methode. Die integrierte Funktion staticmethod() verpackt ihr Funktionsargument in eine spezielle Art von Deskriptor, dessen __get__()-Methode die ursprüngliche Funktion unverändert zurückgibt. Ohne dies hätte die __get__()-Methode von Standard-Funktionsobjekten ein gebundenes Methodenobjekt für 'c.foo' und ein ungebundenes Methodenobjekt für 'C.foo' erstellt.
(XXX Barry schlägt vor, "sharedmethod" anstelle von "staticmethod" zu verwenden, da das Wort statisch bereits auf so viele Arten überladen wird. Aber ich bin mir nicht sicher, ob "shared" die richtige Bedeutung vermittelt.)
Klassenmethoden verwenden ein ähnliches Muster, um Methoden zu deklarieren, die ein implizites erstes Argument erhalten, das die **Klasse** ist, für die sie aufgerufen werden. Dies hat keine C++- oder Java-Entsprechung und ist nicht ganz dasselbe wie Klassenmethoden in Smalltalk, kann aber einen ähnlichen Zweck erfüllen. Laut Armin Rigo ähneln sie "virtuellen Klassenmethoden" im Borland Pascal-Dialekt Delphi. (Python hat auch echte Metaklassen, und vielleicht haben Methoden, die in einer Metaklasse definiert sind, mehr Recht auf den Namen "Klassenmethode"; aber ich erwarte, dass die meisten Programmierer keine Metaklassen verwenden werden.) Hier ist ein Beispiel:
class C:
def foo(cls, y):
print "classmethod", cls, y
foo = classmethod(foo)
C.foo(1)
c = C()
c.foo(1)
Sowohl der Aufruf C.foo(1) als auch der Aufruf c.foo(1) rufen foo() mit **zwei** Argumenten auf und geben "classmethod __main__.C 1" aus. Das erste Argument von foo() ist impliziert, und es ist die Klasse, auch wenn die Methode über eine Instanz aufgerufen wurde. Lassen Sie uns nun das Beispiel fortsetzen:
class D(C):
pass
D.foo(1)
d = D()
d.foo(1)
Dies gibt jedes Mal "classmethod __main__.D 1" aus; mit anderen Worten, die Klasse, die als erstes Argument von foo() übergeben wird, ist die Klasse, die am Aufruf beteiligt ist, nicht die Klasse, die an der Definition von foo() beteiligt ist.
Aber beachten Sie dies:
class E(C):
def foo(cls, y): # override C.foo
print "E.foo() called"
C.foo(y)
foo = classmethod(foo)
E.foo(1)
e = E()
e.foo(1)
In diesem Beispiel wird der Aufruf von C.foo() aus E.foo() die Klasse C als erstes Argument sehen, nicht die Klasse E. Dies ist zu erwarten, da der Aufruf die Klasse C angibt. Dies unterstreicht jedoch den Unterschied zwischen diesen Klassenmethoden und Methoden, die in Metaklassen definiert sind, wo ein Aufruf an eine Metamethode die Zielklasse als explizites erstes Argument übergeben würde. (Wenn Sie das nicht verstehen, machen Sie sich keine Sorgen, Sie sind nicht allein.) Beachten Sie, dass der Aufruf cls.foo(y) ein Fehler wäre – er würde eine unendliche Rekursion verursachen. Beachten Sie auch, dass Sie kein explizites 'cls'-Argument an eine Klassenmethode übergeben können. Wenn Sie dies wünschen (z. B. die Methode __new__ in PEP 253 erfordert dies), verwenden Sie stattdessen eine statische Methode mit einer Klasse als explizitem ersten Argument.
C API
XXX Der folgende Text ist SEHR roh, und ich habe ihn für ein anderes Publikum geschrieben; ich muss ihn noch überarbeiten. XXX Er geht auch nicht detailliert genug auf die C-API ein.
Ein integrierter Typ kann spezielle Datenattribute auf zwei Arten deklarieren: mithilfe einer struct memberlist (definiert in structmember.h) oder einer struct getsetlist (definiert in descrobject.h). Die struct memberlist ist ein alter Mechanismus, der neu verwendet wird: Jedes Attribut hat einen Deskriptor-Datensatz, einschließlich seines Namens, einer Enum, die seinen Typ angibt (verschiedene C-Typen werden unterstützt, ebenso wie PyObject *), eines Offsets vom Anfang der Instanz und eines Schreibschutz-Flags.
Der struct getsetlist-Mechanismus ist neu und für Fälle gedacht, die nicht in dieses Schema passen, da sie zusätzliche Überprüfung erfordern oder einfach berechnete Attribute sind. Jedes Attribut hier hat einen Namen, einen Zeiger auf eine C-Getter-Funktion, einen Zeiger auf eine C-Setter-Funktion und einen Kontextzeiger. Die Funktionszeiger sind optional, sodass beispielsweise das Setzen des Setter-Funktionszeigers auf NULL ein schreibgeschütztes Attribut erzeugt. Der Kontextzeiger dient dazu, zusätzliche Informationen an generische Getter/Setter-Funktionen zu übergeben, aber ich habe dafür noch keinen Bedarf gefunden.
Beachten Sie, dass es auch einen ähnlichen Mechanismus zur Deklaration von integrierten Methoden gibt: Dies sind PyMethodDef-Strukturen, die einen Namen und einen Zeiger auf eine C-Funktion (und einige Flags für die Aufrufkonvention) enthalten.
Traditionell mussten integrierte Typen ihre eigenen tp_getattro und tp_setattro Slot-Funktionen definieren, um diese Attributdefinitionen zum Funktionieren zu bringen (PyMethodDef und struct memberlist sind ziemlich alt). Es gibt Hilfsfunktionen, die ein Array von PyMethodDef- oder Memberlist-Strukturen, ein Objekt und einen Attributnamen entgegennehmen und das Attribut zurückgeben oder setzen, wenn es in der Liste gefunden wird, oder eine Ausnahme auslösen, wenn es nicht gefunden wird. Aber diese Hilfsfunktionen mussten explizit von der tp_getattro- oder tp_setattro-Methode des spezifischen Typs aufgerufen werden, und sie führten eine lineare Suche des Arrays mithilfe von strcmp() durch, um das Array-Element zu finden, das das angeforderte Attribut beschreibt.
Ich habe jetzt einen brandneuen generischen Mechanismus, der diese Situation erheblich verbessert.
- Zeiger auf Arrays von
PyMethodDef, memberlist und getsetlist-Strukturen sind Teil des neuen Typobjekts (tp_methods,tp_members,tp_getset). - Zum Zeitpunkt der Typinitialisierung (in
PyType_InitDict()) wird für jeden Eintrag in diesen drei Arrays ein Deskriptorobjekt erstellt und in einem Dictionary platziert, das zum Typ gehört (tp_dict). - Deskriptoren sind sehr schlanke Objekte, die hauptsächlich auf die entsprechende Struktur verweisen. Eine Implementierungsdetails ist, dass alle Deskriptoren denselben Objekttyp teilen und ein Diskriminierungsfeld angibt, welche Art von Deskriptor es ist (Methode, Mitglied oder getset).
- Wie in PEP 252 erklärt, haben Deskriptoren eine
get()-Methode, die ein Objektargument entgegennimmt und das Attribut dieses Objekts zurückgibt; Deskriptoren für beschreibbare Attribute haben auch eineset()-Methode, die ein Objekt und einen Wert entgegennimmt und das Attribut dieses Objekts setzt. Beachten Sie, dass dasget()-Objekt auch alsbind()-Operation für Methoden dient und die ungebundene Methodenimplementierung an das Objekt bindet. - Anstatt eigene tp_getattro- und tp_setattro-Implementierungen bereitzustellen, platzieren fast alle integrierten Objekte jetzt
PyObject_GenericGetAttrund (wenn sie beschreibbare Attribute haben)PyObject_GenericSetAttrin ihrentp_getattro- undtp_setattro-Slots. (Oder sie können dieseNULLbelassen und sie vom Standard-Basisobjekt erben, wenn sie vor der Erstellung der ersten Instanz einen expliziten Aufruf vonPyType_InitDict()für den Typ arrangieren.) - Im einfachsten Fall führt
PyObject_GenericGetAttr()genau eine Dictionary-Suche durch: Sie sucht den Attributnamen im Dictionary des Typs (obj->ob_type->tp_dict). Bei Erfolg gibt es zwei Möglichkeiten: Der Deskriptor hat eine get-Methode oder nicht. Zur Beschleunigung sind die get- und set-Methoden Typ-Slots:tp_descr_getundtp_descr_set. Wenn dertp_descr_get-Slot nicht NULL ist, wird er aufgerufen, wobei das Objekt als einziges Argument übergeben wird, und der Rückgabewert dieses Aufrufs ist das Ergebnis der getattr-Operation. Wenn dertp_descr_get-SlotNULList, wird als Fallback der Deskriptor selbst zurückgegeben (vergleichen Sie Klassenattribute, die keine Methoden, sondern einfache Werte sind). PyObject_GenericSetAttr()funktioniert sehr ähnlich, verwendet jedoch dentp_descr_setSlot und ruft ihn mit dem Objekt und dem neuen Attributwert auf; wenn dertp_descr_setSlotNULList, wird einAttributeErrorausgelöst.- Aber nun zu einem komplizierteren Fall. Der oben beschriebene Ansatz eignet sich für die meisten integrierten Objekte wie Listen, Zeichenketten und Zahlen. Einige Objekttypen haben jedoch in jeder Instanz ein Dictionary, das beliebige Attribute speichern kann. Tatsächlich erhalten Sie automatisch ein solches Dictionary, wenn Sie eine Klassendefinition verwenden, um einen vorhandenen integrierten Typ zu untertypisieren (es sei denn, Sie schalten es explizit ab, indem Sie eine weitere fortgeschrittene Funktion,
__slots__, verwenden). Nennen wir dies das Instanz-Dictionary, um es vom Typ-Dictionary zu unterscheiden. - Im komplizierteren Fall gibt es einen Konflikt zwischen Namen, die im Instanz-Dictionary gespeichert sind, und Namen, die im Typ-Dictionary gespeichert sind. Wenn beide Dictionaries einen Eintrag mit demselben Schlüssel haben, welcher soll dann zurückgegeben werden? Wenn ich mir das klassische Python als Leitfaden ansehe, finde ich widersprüchliche Regeln: für Klasseninstanzen überschreibt das Instanz-Dictionary das Klassen-Dictionary, außer für die Sonderattribute (wie
__dict__und__class__), die Vorrang vor dem Instanz-Dictionary haben. - Ich habe dies mit der folgenden Regelmenge gelöst, die in
PyObject_GenericGetAttr()implementiert ist.- Suchen Sie im Typ-Dictionary. Wenn Sie einen Daten-Deskriptor finden, verwenden Sie dessen
get()-Methode, um das Ergebnis zu erzeugen. Dies kümmert sich um Sonderattribute wie__dict__und__class__. - Suchen Sie im Instanz-Dictionary. Wenn Sie etwas finden, ist das alles. (Dies erfüllt die Anforderung, dass das Instanz-Dictionary normalerweise das Klassen-Dictionary überschreibt.)
- Suchen Sie erneut im Typ-Dictionary (in Wirklichkeit wird hier natürlich das gespeicherte Ergebnis aus Schritt 1 verwendet). Wenn Sie einen Deskriptor finden, verwenden Sie dessen
get()-Methode; wenn Sie etwas anderes finden, ist das alles; wenn es nicht da ist, lösen SieAttributeErroraus.
Dies erfordert eine Klassifizierung von Deskriptoren als Daten- und Nicht-Daten-Deskriptoren. Die aktuelle Implementierung klassifiziert vernünftigerweise Member- und Getset-Deskriptoren als Daten (auch wenn sie schreibgeschützt sind!) und Methoden-Deskriptoren als Nicht-Daten. Nicht-Deskriptoren (wie Funktionszeiger oder einfache Werte) werden ebenfalls als Nicht-Daten klassifiziert (!).
- Suchen Sie im Typ-Dictionary. Wenn Sie einen Daten-Deskriptor finden, verwenden Sie dessen
- Dieses Schema hat einen Nachteil: Im von mir angenommenen häufigsten Fall, der Referenzierung einer Instanzvariablen, die im Instanz-Dictionary gespeichert ist, werden zwei Dictionary-Lookups durchgeführt, während das klassische Schema einen schnellen Test für Attribute, die mit zwei Unterstrichen beginnen, plus einen einzigen Dictionary-Lookup durchführte. (Obwohl die Implementierung leider so strukturiert ist, dass
instance_getattr()instance_getattr1()aufruft, wasinstance_getattr2()aufruft, was schließlichPyDict_GetItem()aufruft, und der Unterstrich-TestPyString_AsString()anstelle der Inline-Variante aufruft. Ich frage mich, ob es nicht eine gute Idee wäre, dies stark zu optimieren, um Python 2.2 zu beschleunigen, wenn wir es nicht alles herausreißen würden. :-) - Ein Benchmark bestätigt, dass dies tatsächlich so schnell ist wie die klassische Instanzvariablen-Suche, daher mache ich mir keine Sorgen mehr.
- Modifikation für dynamische Typen: Schritt 1 und 3 suchen im Dictionary des Typs und all seinen Basisklassen (in MRO-Sequenz, natürlich).
Diskussion
XXX
Beispiele
Betrachten wir Listen. Im klassischen Python waren die Methodennamen von Listen über das Attribut __methods__ von Listenobjekten verfügbar.
>>> [].__methods__
['append', 'count', 'extend', 'index', 'insert', 'pop',
'remove', 'reverse', 'sort']
>>>
Unter dem neuen Vorschlag existiert das Attribut __methods__ nicht mehr.
>>> [].__methods__
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'list' object has no attribute '__methods__'
>>>
Stattdessen können Sie dieselben Informationen vom Listentyp erhalten.
>>> T = [].__class__
>>> T
<type 'list'>
>>> dir(T) # like T.__dict__.keys(), but sorted
['__add__', '__class__', '__contains__', '__eq__', '__ge__',
'__getattr__', '__getitem__', '__getslice__', '__gt__',
'__iadd__', '__imul__', '__init__', '__le__', '__len__',
'__lt__', '__mul__', '__ne__', '__new__', '__radd__',
'__repr__', '__rmul__', '__setitem__', '__setslice__', 'append',
'count', 'extend', 'index', 'insert', 'pop', 'remove',
'reverse', 'sort']
>>>
Die neue Introspektions-API liefert mehr Informationen als die alte: Zusätzlich zu den regulären Methoden werden auch die Methoden angezeigt, die normalerweise über spezielle Notationen aufgerufen werden, z. B. __iadd__ (+=), __len__ (len), __ne__ (!=). Sie können jede Methode aus dieser Liste direkt aufrufen.
>>> a = ['tic', 'tac']
>>> T.__len__(a) # same as len(a)
2
>>> T.append(a, 'toe') # same as a.append('toe')
>>> a
['tic', 'tac', 'toe']
>>>
Dies ist genauso wie bei benutzerdefinierten Klassen.
Beachten Sie einen vertrauten, aber überraschenden Namen in der Liste: __init__. Dies ist der Bereich von PEP 253.
Abwärtskompatibilität
XXX
Warnungen und Fehler
XXX
Implementierung
Eine Teilimplementierung dieser PEP ist über CVS als Zweig namens "descr-branch" verfügbar. Um diese Implementierung auszuprobieren, laden Sie Python von CVS gemäß den Anweisungen unter http://sourceforge.net/cvs/?group_id=5470 herunter, fügen Sie jedoch die Argumente "-r descr-branch" zum cvs checkout-Befehl hinzu. (Sie können auch mit einem vorhandenen Checkout beginnen und "cvs update -r descr-branch" ausführen.) Einige Beispiele für die hier beschriebenen Funktionen finden Sie in der Datei Lib/test/test_descr.py.
Hinweis: Der Code in diesem Zweig geht weit über diese PEP hinaus; er ist auch der Experimentierbereich für PEP 253 (Subtyping Built-in Types).
Referenzen
XXX
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0252.rst
Zuletzt geändert: 2025-02-01 08:55:40 GMT