Vereinheitlichung von Typen und Klassen in Python 2.2
Vereinheitlichung von Typen und Klassen in Python 2.2
Python Version: 2.2
(Für eine neuere Version dieses Tutorials siehe Python 2.2.3)
Guido van Rossum
Dieses Papier ist ein unvollständiger Entwurf. Ich bitte um Feedback. Wenn Sie Probleme finden, schreiben Sie mir bitte an guido@python.org.
Inhaltsverzeichnis
- Einleitung
- Unterklassen von integrierten Typen
- Integrierte Typen als Factory-Funktionen
- Introspektion von Instanzen integrierter Typen
- Statische Methoden und Klassenmethoden
- Properties: Attribute, die durch Get/Set-Methoden definiert werden
- Methodenauflösungsreihenfolge (Method Resolution Order)
- Kooperative Methoden und "super"
- Überschreiben der __new__-Methode
- Metaklassen
- Rückwärtsinkompatibilitäten
- Referenzen
Einleitung
Python 2.2 führt die erste Phase der "Typ/Klassen-Vereinheitlichung" ein. Dies ist eine Reihe von Änderungen an Python, die darauf abzielen, die meisten Unterschiede zwischen integrierten Typen und benutzerdefinierten Klassen zu beseitigen. Vielleicht die offensichtlichste ist die Einschränkung, integrierte Typen (wie den Typ von Listen und Dictionaries) nicht als Basisklasse in einer Klassendefinition zu verwenden.
Dies ist eine der größten Änderungen, die Python je erfahren hat, und dennoch kann sie mit sehr wenigen Rückwärtsinkompatibilitäten durchgeführt werden. Die Änderungen werden im Detail in einer Reihe von PEPs (Python Enhancement Proposals) beschrieben. PEPs sind nicht als Tutorials konzipiert, und die PEPs, die die Typ/Klassen-Vereinheitlichung beschreiben, sind manchmal schwer zu lesen. Sie sind auch noch nicht fertig. Hier kommt dieses Papier ins Spiel: Es stellt die Schlüsselelemente der Typ/Klassen-Vereinheitlichung für den durchschnittlichen Python-Programmierer vor.
Ein kleiner Begriff: "klassisches Python" bezieht sich auf Python 2.1 (und seine Patch-Releases wie 2.1.1) oder frühere Versionen, während sich "klassische Klassen" auf Klassen beziehen, die mit einer Klassendefinition definiert wurden, die kein integriertes "object" unter ihren Basen hat: entweder weil sie keine Basen hat, oder weil alle ihre Basen selbst klassische Klassen sind – angewendet auf die Definition rekursiv.
Klassische Klassen sind in Python 2.2 immer noch eine Sonderkategorie. Schließlich werden sie vollständig mit Typen vereinheitlicht, aber aufgrund zusätzlicher Rückwärtsinkompatibilitäten wird dies nach der Veröffentlichung von 2.2 geschehen (vielleicht nicht vor Python 3.0). Ich werde versuchen, "Typ" zu sagen, wenn ich einen integrierten Typ meine, und "Klasse", wenn ich mich auf eine klassische Klasse oder etwas beziehe, das beides sein könnte; wenn es aus dem Kontext nicht klar wäre, welche Interpretation gemeint ist, werde ich versuchen, explizit zu sein, indem ich "klassische Klasse" oder "Klasse oder Typ" verwende.
Unterklassen von integrierten Typen
Beginnen wir mit dem pikantesten Teil: Sie können integrierte Typen wie Dictionaries und Listen unterklassifizieren. Alles, was Sie brauchen, ist ein Name für eine Basisklasse, die ein integrierter Typ ist, und Sie sind dabei.
Es gibt einen neuen integrierten Namen, "dict", für den Typ von Dictionaries. (In Version 2.2b1 und früher hieß dieser "dictionary"; obwohl ich generell keine Abkürzungen mag, war "dictionary" einfach zu lang zum Tippen, und wir sagen schon seit Jahren "dict".)
Dies ist eigentlich nur Zucker, da es bereits zwei andere Möglichkeiten gibt, diesen Typ zu benennen: type({}) und (nachdem das types-Modul importiert wurde) types.DictType (und eine dritte, types.DictionaryType). Aber jetzt, wo Typen eine zentralere Rolle spielen, scheint es angebracht, integrierte Namen für die Typen zu haben, denen Sie wahrscheinlich begegnen werden.
Hier ist ein Beispiel für eine einfache dict-Unterklasse, die einen "Standardwert" bereitstellt, der zurückgegeben wird, wenn auf einen fehlenden Schlüssel zugegriffen wird
class defaultdict(dict):
def __init__(self, default=None):
dict.__init__(self)
self.default = default
def __getitem__(self, key):
try:
return dict.__getitem__(self, key)
except KeyError:
return self.default
Dieses Beispiel zeigt einige Dinge. Die __init__() Methode erweitert die dict.__init__() Methode. Wie __init__() Methoden es nun mal tun, hat sie eine andere Argumentenliste als die __init__() Methode der Basisklasse. Ebenso erweitert die __getitem__() Methode die __getitem__() Methode der Basisklasse.
Die __getitem__() Methode könnte auch wie folgt geschrieben werden, unter Verwendung des neuen Tests "key in dict", der in Python 2.2 eingeführt wurde
def __getitem__(self, key):
if key in self:
return dict.__getitem__(self, key)
else:
return self.default
Ich glaube, dass diese Version weniger effizient ist, da sie die Schlüsselsuche zweimal durchführt. Die Ausnahme wäre, wenn wir erwarten, dass der angeforderte Schlüssel fast nie im Dictionary vorhanden ist: Dann ist das Einrichten der try/except-Anweisung teurer als der fehlschlagende "key in self"-Test.
Um vollständig zu sein, sollte die get()-Methode wahrscheinlich ebenfalls erweitert werden, damit sie denselben Standard wie __getitem__() verwendet
def get(self, key, *args):
if not args:
args = (self.default,)
return dict.get(self, key, *args)
(Obwohl diese Funktion mit einer Variable-Length-Argumentliste deklariert ist, sollte sie eigentlich nur mit einem oder zwei Argumenten aufgerufen werden; wenn mehr übergeben werden, löst der Methodenaufruf der Basisklasse eine TypeError-Ausnahme aus.)
Wir sind nicht darauf beschränkt, Methoden zu erweitern, die auf der Basisklasse definiert sind. Hier ist eine nützliche Methode, die etwas Ähnliches wie update() tut, aber vorhandene Werte beibehält, anstatt sie mit neuen Werten zu überschreiben, wenn ein Schlüssel in beiden Dictionaries vorhanden ist
def merge(self, other):
for key in other:
if key not in self:
self[key] = other[key]
Dies verwendet den neuen "key not in dict"-Test sowie die neue "for key in dict:"-Schleife, um effizient (ohne eine Kopie der Schlüsselliste zu erstellen) über alle Schlüssel in einem Dictionary zu iterieren. Es erfordert nicht, dass das andere Argument ein defaultdict oder sogar ein Dictionary ist: jedes Mapping-Objekt, das "for key in other" und other[key] unterstützt, ist ausreichend.
Hier ist der neue Typ in Aktion
>>> print defaultdict # show our type
<class '__main__.defaultdict'>
>>> print type(defaultdict) # its metatype
<type 'type'>
>>> a = defaultdict(default=0.0) # create an instance
>>> print a # show the instance
{}
>>> print type(a) # show its type
<class '__main__.defaultdict'>
>>> print a.__class__ # show its class
<class '__main__.defaultdict'>
>>> print type(a) is a.__class__ # its type is its class
1
>>> a[1] = 3.25 # modify the instance
>>> print a # show the new value
{1: 3.25}
>>> print a[1] # show the new item
3.25
>>> print a[0] # a non-existant item
0.0
>>> a.merge({1:100, 2:200}) # use a dictionary method
>>> print a # show the result
{1: 3.25, 2: 200}
>>>
Wir können den neuen Typ auch in Kontexten verwenden, in denen klassische Klassen nur "echte" Dictionaries zulassen, wie z. B. die locals/globals-Dictionaries für die exec-Anweisung oder die integrierte Funktion eval()
>>> print a.keys()
[1, 2]
>>> exec "x = 3; print x" in a
3
>>> print a.keys()
['__builtins__', 1, 2, 'x']
>>> print a['x']
3
>>>
Unsere __getitem__() Methode wird jedoch nicht für den Variablenzugriff durch den Interpreter verwendet
>>> exec "print foo" in a
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<string>", line 1, in ?
NameError: name 'foo' is not defined
>>>
Warum wird hier nicht 0.0 ausgegeben? Der Interpreter verwendet eine interne Funktion, um auf das Dictionary zuzugreifen, die unsere __getitem__() Überschreibung umgeht. Ich gebe zu, dass dies ein Problem sein kann (obwohl es *nur* in diesem Kontext ein Problem ist, wenn eine dict-Unterklasse als locals/globals-Dictionary verwendet wird); es bleibt abzuwarten, ob ich dies beheben kann, ohne die Leistung im häufigen Fall zu beeinträchtigen.
Nun sehen wir, dass defaultdict-Instanzen dynamische Instanzvariablen haben, genau wie klassische Klassen
>>> a.default = -1
>>> print a["noway"]
-1
>>> a.default = -1000
>>> print a["noway"]
-1000
>>> print a.__dict__.keys()
['default']
>>> a.x1 = 100
>>> a.x2 = 200
>>> print a.x1
100
>>> print a.__dict__.keys()
['default', 'x2', 'x1']
>>> print a.__dict__
{'default': -1000, 'x2': 200, 'x1': 100}
>>>
Dies ist nicht immer das, was Sie wollen; insbesondere verdoppelt die Verwendung eines separaten Dictionaries, um eine einzelne Instanzvariable zu speichern, den Speicherverbrauch einer defaultdict-Instanz im Vergleich zur Verwendung eines regulären Dictionaries! Es gibt eine Möglichkeit, dies zu vermeiden
class defaultdict2(dict):
__slots__ = ['default']
def __init__(self, default=None):
...(like before)...
Die __slots__-Deklaration nimmt eine Liste von Instanzvariablen entgegen und reserviert im Speicherplatz für genau diese im Instanz. Wenn __slots__ verwendet wird, können keine anderen Instanzvariablen zugewiesen werden
>>> a = defaultdict2(default=0.0)
>>> a[1]
0.0
>>> a.default = -1
>>> a[1]
-1
>>> a.x1 = 1
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'defaultdict2' object has no attribute 'x1'
>>>
Einige bemerkenswerte Kleinigkeiten und Warnungen zu __slots__
- Eine undefinierte Slot-Variable löst wie erwartet AttributeError aus. (Beachten Sie, dass in Python 2.2b2 und früher Slot-Variablen standardmäßig den Wert None hatten und "löschen" diesen Standardwert wiederherstellt.)
- Sie können kein Klassenattribut verwenden, um einen Standardwert für eine Instanzvariable zu definieren, die durch __slots__ definiert ist; die __slots__-Deklaration erstellt für jede Variable ein Klassenattribut, das einen Deskriptor enthält, und das Zuweisen eines Klassenattributs mit einem Standardwert würde diesen Deskriptor überschreiben.
- Es gibt keine Prüfung, die Sie daran hindert, eine Instanzvariable zu überschreiben, die bereits durch eine Basisklasse mit einer __slots__-Deklaration definiert wurde. Wenn Sie dies tun, ist die von der Basisklasse definierte Instanzvariable unzugänglich (außer durch Abrufen ihres Deskriptors direkt von der Basisklasse; dies könnte verwendet werden, um sie umzubenennen). Dies macht die Bedeutung Ihres Programms undefiniert; möglicherweise wird in Zukunft eine Prüfung hinzugefügt, um dies zu verhindern.
- Instanzen einer Klasse, die __slots__ verwendet, haben kein __dict__ (es sei denn, eine Basisklasse definiert ein __dict__); aber Instanzen abgeleiteter Klassen davon haben ein __dict__, es sei denn, ihre Klasse verwendet ebenfalls __slots__.
- Sie können ein Objekt ohne Instanzvariablen und ohne __dict__ definieren, indem Sie __slots__ = [] verwenden.
- Sie können keine Slots mit "variablenlangen" integrierten Typen als Basisklasse verwenden. Variable-lange integrierte Typen sind long, str und tuple.
- Eine Klasse, die __slots__ verwendet, unterstützt keine schwachen Referenzen auf ihre Instanzen, es sei denn, eine der Zeichenketten in der __slots__-Liste hat den Wert "__weakref__". (Hmm, diese Funktion könnte auf "__dict__" erweitert werden...)
- Die __slots__-Variable muss keine Liste sein; jedes Nicht-Zeichenkettenobjekt, über das iteriert werden kann, ist ausreichend, und die zurückgegebenen Werte der Iteration werden als Slot-Namen verwendet. Insbesondere kann ein Dictionary verwendet werden. Sie können auch eine einzelne Zeichenkette verwenden, um einen einzelnen Slot zu deklarieren. In Zukunft kann jedoch eine zusätzliche Bedeutung der Verwendung eines Dictionaries zugewiesen werden, zum Beispiel können die Dictionary-Werte verwendet werden, um den Typ einer Instanzvariable zu beschränken oder eine Doc-String bereitzustellen; die Auswirkung der Verwendung von etwas, das keine Liste ist, macht die Bedeutung Ihres Programms undefiniert.
Beachten Sie, dass, obwohl im Allgemeinen die Überladung von Operatoren genauso funktioniert wie bei klassischen Klassen, es einige Unterschiede gibt. (Der größte ist das Fehlen der Unterstützung für __coerce__; New-Style-Klassen sollten immer die neue numerische API verwenden, die den anderen Operanden ungekoerzt an die __add__- und __radd__-Methoden usw. übergibt.)
Es gibt eine neue Möglichkeit, Attributzugriffe zu überschreiben. Der __getattr__-Hook, falls definiert, funktioniert genauso wie bei klassischen Klassen: Er wird nur aufgerufen, wenn die normale Suche nach dem Attribut es nicht findet. Aber Sie können jetzt auch __getattribute__ überschreiben, eine neue Operation, die für *alle* Attributreferenzen aufgerufen wird.
Beim Überschreiben von __getattribute__ sollten Sie bedenken, dass es leicht zu unendlicher Rekursion kommen kann: wann immer __getattribute__ auf ein Attribut von self verweist (selbst auf self.__dict__!), wird es rekursiv aufgerufen. (Dies ähnelt __setattr__, das bei allen Attributzuweisungen aufgerufen wird; __getattr__ kann dies ebenfalls erleiden, wenn es unachtsam geschrieben ist und auf ein nicht vorhandenes Attribut von self verweist.)
Der richtige Weg, um innerhalb von __getattribute__ auf ein beliebiges Attribut von self zuzugreifen, ist der Aufruf der __getattribute__-Methode der Basisklasse, genauso wie jede Methode, die eine Basisklassenmethode überschreibt, die Basisklassenmethode aufrufen kann: Base.__getattribute__(self, name). (Siehe auch die Diskussion über super() unten, wenn Sie in einer Welt mit Mehrfachvererbung korrekt sein wollen.)
Hier ist ein Beispiel für das Überschreiben von __getattribute__ (tatsächlich die Erweiterung, da die überschreibende Methode die Basisklassenmethode aufruft)
class C(object):
def __getattribute__(self, name):
print "accessing %r.%s" % (self, name)
return object.__getattribute__(self, name)
Ein Hinweis zu __setattr__: manchmal werden Attribute nicht in self.__dict__ gespeichert (z. B. bei Verwendung von __slots__ oder Properties, oder bei Verwendung einer integrierten Basisklasse). Das gleiche Muster wie für __getattribute__ gilt, wobei Sie die Basisklassen __setattr__ aufrufen, um die eigentliche Arbeit zu erledigen. Hier ist ein Beispiel
class C(object):
def __setattr__(self, name, value):
if hasattr(self, name):
raise AttributeError, "attributes are write-once"
object.__setattr__(self, name, value)
C++-Programmierer finden es vielleicht nützlich zu erkennen, dass diese Form der Unterklassifizierung in Python sehr ähnlich der einfachen Vererbung in C++ implementiert ist, wobei __class__ die Rolle der vtable spielt.
Es gibt noch viel mehr, was erklärt werden könnte (wie die __metaclass__-Deklaration und die __new__-Methode), aber das meiste davon ist ziemlich esoterisch. Siehe unten, wenn Sie interessiert sind.
Ich beende mit einer Liste von Vorbehalten
- Sie können Mehrfachvererbung verwenden, aber Sie können nicht von verschiedenen integrierten Typen mehrfach erben (zum Beispiel können Sie keinen Typ erstellen, der sowohl von den integrierten dict- als auch von den list-Typen erbt). Dies ist eine permanente Einschränkung; es würde zu viele Änderungen an Pythons Objektimplementierung erfordern, um sie aufzuheben. Sie können jedoch Mix-in-Klassen erstellen, indem Sie von "object" erben. Dies ist ein neuer integrierter Typ, der den featurelosen Basistyp aller integrierten Typen im neuen System benennt.
- Bei der Verwendung von Mehrfachvererbung können Sie klassische Klassen und integrierte Typen (oder von integrierten Typen abgeleitete Typen) in der Liste der Basisklassen mischen. (Dies ist neu in Python 2.2b2; in früheren Versionen war dies nicht möglich.)
- Siehe auch die allgemeine Liste der Fehler in 2.2.
Integrierte Typen als Factory-Funktionen
Der vorherige Abschnitt zeigte, dass eine Instanz des integrierten Subtyps defaultdict durch Aufruf von defaultdict() erstellt werden kann. Dies ist erwartungsgemäß, da dies auch für klassische Klassen funktioniert. Aber hier ist ein neues Feature: integrierte Basistypen selbst können auch instanziiert werden, indem der Typ direkt aufgerufen wird.
Für mehrere integrierte Typen gibt es bereits Factory-Funktionen, die im klassischen Python nach dem Typ benannt sind, z. B. str() und int(). Ich habe diese integrierten Funktionen geändert, sodass sie nun Namen für die entsprechenden Typen sind. Obwohl dies den Typ dieser Namen von integrierter Funktion zu integriertem Typ ändert, erwarte ich nicht, dass dies zu Problemen mit der Rückwärtskompatibilität führt: Ich habe sichergestellt, dass die Typen mit genau denselben Argumentenlisten aufgerufen werden können wie die ehemaligen Funktionen. (Sie können in der Regel auch ohne Argumente aufgerufen werden, was ein Objekt mit einem geeigneten Standardwert ergibt, z. B. Null oder leer; dies ist neu.)
Dies sind die betroffenen integrierten Funktionen
- int([number_or_string[, base_number]])
- long([number_or_string])
- float([number_or_string])
- complex([number_or_string[, imag_number]])
- str([object])
- unicode([string[, encoding_string]])
- tuple([iterable])
- list([iterable])
- type(object) or type(name_string, bases_tuple, methods_dict)
Die Signatur von type() erfordert eine Erklärung: traditionell gibt type(x) den Typ des Objekts x zurück, und diese Verwendung wird immer noch unterstützt. type(name, bases, methods) ist jedoch eine neue Verwendung, die ein brandneues Typobjekt erstellt. (Dies gehört zur Metaklassen-Programmierung, und ich werde hier nicht weiter darauf eingehen, außer zu bemerken, dass diese Signatur dieselbe ist wie die, die vom Don Beaudry Hook der Metaklassen-Berühmtheit verwendet wird.)
Es gibt auch einige neue integrierte Funktionen, die demselben Muster folgen. Diese wurden oben beschrieben oder werden unten beschrieben
- dict([mapping_or_iterable]) - gibt ein neues Dictionary zurück; das optionale Argument muss entweder ein Mapping sein, dessen Elemente kopiert werden, oder eine Sequenz von 2-Tupeln der Länge 2, die die (Schlüssel, Wert)-Paare für die Einfügung in das neue Dictionary angeben
- object([...]) - gibt ein neues featureloses Objekt zurück; Argumente werden ignoriert
- classmethod(function) - siehe unten
- staticmethod(function) - siehe unten
- super(class_or_type[, instance]) - siehe unten
- property([fget[, fset[, fdel[, doc]]]]) - siehe unten
Der Zweck dieser Änderung ist zweifach. Erstens macht dies die Verwendung eines beliebigen dieser Typen als Basisklasse in einer Klassendefinition bequem. Zweitens erleichtert es die Überprüfung eines bestimmten Typs ein wenig: anstatt type(x) is type(0) zu schreiben, können Sie jetzt isinstance(x, int) schreiben.
Was mich daran erinnert. Das zweite Argument von isinstance() kann nun ein Tupel von Klassen oder Typen sein. Zum Beispiel gibt isinstance(x, (int, long)) True zurück, wenn x ein int oder ein long ist (oder eine Instanz einer Unterklasse davon), und ähnlich testet isinstance(x, (str, unicode)) auf eine Zeichenkette beiderlei Art. Dies haben wir bei isclass() nicht getan.
Introspektion von Instanzen integrierter Typen
Für Instanzen integrierter Typen (und für New-Style-Klassen im Allgemeinen) ist x.__class__ jetzt dasselbe wie type(x)
>>> type([])
<type 'list'>
>>> [].__class__
<type 'list'>
>>> list
<type 'list'>
>>> isinstance([], list)
1
>>> isinstance([], dict)
0
>>> isinstance([], object)
1
>>>
Im klassischen Python waren die Methodennamen von Listen als __methods__-Attribut von listenobjekten verfügbar, mit demselben Effekt wie die Verwendung der integrierten dir()-Funktion
Python 2.1 (#30, Apr 18 2001, 00:47:18)
[GCC egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> [].__methods__
['append', 'count', 'extend', 'index', 'insert', 'pop',
'remove', 'reverse', 'sort']
>>>
>>> dir([])
['append', 'count', 'extend', 'index', 'insert', 'pop',
'remove', 'reverse', 'sort']
Unter dem neuen Vorschlag existiert das __methods__-Attribut nicht mehr
Python 2.2c1 (#803, Dec 13 2001, 23:06:05)
[GCC egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> [].__methods__
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'list' object has no attribute '__methods__'
>>>
Stattdessen können Sie dieselben Informationen von der dir()-Funktion erhalten, die mehr Informationen liefert
>>> dir([])
['__add__', '__class__', '__contains__', '__delattr__',
'__delitem__', '__eq__', '__ge__', '__getattribute__',
'__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__',
'__imul__', '__init__', '__le__', '__len__', '__lt__', '__mul__',
'__ne__', '__new__', '__reduce__', '__repr__', '__rmul__',
'__setattr__', '__setitem__', '__setslice__', '__str__', 'append',
'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse',
'sort']
>>>
Das neue dir() liefert mehr Informationen als das alte: zusätzlich zu den Namen von Instanzvariablen und regulären Methoden zeigt es auch die Methoden an, die normalerweise über spezielle Notationen aufgerufen werden, wie __iadd__ (+=), __len__ (len), __ne__ (!=).
Mehr über die neue dir()-Funktion
- dir() auf einer Instanz (klassisch oder New-Style) zeigt die Instanzvariablen sowie die Methoden und Klassenattribute an, die von der Klasse der Instanz und all ihren Basisklassen definiert wurden.
- dir() auf einer Klasse (klassisch oder New-Style) zeigt den Inhalt des __dict__ der Klasse und all ihrer Basisklassen an. Es werden keine Klassenattribute angezeigt, die von einer Metaklasse definiert wurden.
- dir() auf einem Modul zeigt den Inhalt des __dict__ des Moduls an. (Dies ist unverändert.)
- dir() ohne Argumente zeigt die lokalen Variablen des Aufrufers an. (Ebenfalls unverändert.)
- Es gibt eine neue C-API, die die dir()-Funktion implementiert: PyObject_Dir().
- Es gibt weitere Details; insbesondere für Objekte, die __dict__ oder __class__ überschreiben, werden diese berücksichtigt, und aus Kompatibilitätsgründen werden __members__ und __methods__ berücksichtigt, wenn sie definiert sind.
Sie können eine Methode eines integrierten Typs als "unbound method" verwenden
>>> a = ['tic', 'tac']
>>> list.__len__(a) # same as len(a)
2
>>> list.append(a, 'toe') # same as a.append('toe')
>>> a
['tic', 'tac', 'toe']
>>>
Dies ist genau wie die Verwendung einer unbound method einer benutzerdefinierten Klasse – und ähnlich ist sie meist aus einer Methode einer Unterklasse nützlich, um die entsprechende Basisklassenmethode aufzurufen.
Im Gegensatz zu benutzerdefinierten Klassen können Sie integrierte Typen nicht ändern: Versuche, ein Attribut eines integrierten Typs zuzuweisen, lösen eine TypeError aus, und ihr __dict__ ist ein schreibgeschütztes Proxy-Objekt. Die Einschränkung für Attributzuweisungen wird für New-Style-Benutzerdefinierte Klassen aufgehoben, einschließlich Unterklassen von integrierten Typen; jedoch haben selbst diese ein schreibgeschütztes __dict__-Proxy, und Sie müssen Attributzuweisungen verwenden, um eine Methode einer New-Style-Klasse zu ersetzen oder hinzuzufügen. Beispiel-Sitzung
>>> list.append
<method 'append' of 'list' objects>
>>> list.append = list.append
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: can't set attributes of built-in/extension type 'list'
>>> list.answer = 42
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: can't set attributes of built-in/extension type 'list'
>>> list.__dict__['append']
<method 'append' of 'list' objects>
>>> list.__dict__['answer'] = 42
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: object does not support item assignment
>>> class L(list):
... pass
...
>>> L.append = list.append
>>> L.answer = 42
>>> L.__dict__['answer']
42
>>> L.__dict__['answer'] = 42
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: object does not support item assignment
>>>
Für Neugierige: Es gibt zwei Gründe, warum das Ändern integrierter Klassen nicht erlaubt ist. Erstens wäre es zu einfach, eine Invariante eines integrierten Typs zu brechen, auf die anderswo entweder von der Standardbibliothek oder vom Laufzeitcode verwiesen wird. Zweitens werden, wenn Python in eine andere Anwendung eingebettet ist, die mehrere Python-Interpreter erstellt, die integrierten Klassenobjekte (als statisch zugewiesene Datenstrukturen) zwischen allen Interpretern geteilt; somit könnte Code, der in einem Interpreter läuft, einen anderen Interpreter in Aufruhr versetzen, was ein No-Go ist.
Statische Methoden und Klassenmethoden
Die neue Deskriptor-API macht es möglich, statische Methoden und Klassenmethoden hinzuzufügen. Statische Methoden sind leicht zu beschreiben: sie verhalten sich ziemlich genau 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() wird kein "self" deklariert, und es wird keine Instanz für den Aufruf benötigt. Wenn eine Instanz verwendet wird, wird sie nur dazu verwendet, die Klasse zu finden, die die statische Methode definiert. Dies funktioniert für klassische und neue Klassen!
Die Zeile "foo = staticmethod(foo)" in der Klassendefinition ist das entscheidende Element: dies macht foo() zu einer statischen Methode. Die integrierte staticmethod() umschließt ihr Funktionsargument in eine spezielle Art von Deskriptor, deren __get__() Methode die ursprüngliche Funktion unverändert zurückgibt.
Mehr zu __get__ Methoden: In Python 2.2 wird der Zauber der Bindung von Methoden an Instanzen (auch für klassische Klassen!) über die __get__ Methode des Objekts in der Klasse durchgeführt. Die __get__ Methode für reguläre Funktionsobjekte gibt ein gebundenes Methodenobjekt zurück; die __get__ Methode für staticfunction-Objekte gibt die zugrundeliegende Funktion zurück. Wenn ein Klassenattribut keine __get__ Methode hat, wird es nie an eine Instanz gebunden, oder anders ausgedrückt, es gibt eine Standard __get__ Operation, die das Objekt unverändert zurückgibt; so werden einfache Klassenvariablen (z. B. numerische Werte) behandelt.
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. (Python hat auch echte Metaklassen, und vielleicht haben Methoden, die in einer Metaklasse definiert sind, mehr Recht auf den Namen "Klassenmethode"; aber ich gehe davon aus, 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 letztendlich 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. Nun setzen wir das Beispiel fort
class D(C):
pass
D.foo(1)
d = D()
d.foo(1)
Dies gibt beide Male "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() von E.foo() aus die Klasse C als erstes Argument sehen, nicht die Klasse E. Dies ist zu erwarten, da der Aufruf die Klasse C angibt. Aber es unterstreicht den Unterschied zwischen diesen Klassenmethoden und Methoden, die in Metaklassen definiert sind, wo ein Upcall zu einer Metamethode die Zielklasse als explizites erstes Argument übergeben würde. (Wenn Sie das nicht verstehen, machen Sie sich keine Sorgen, Sie sind nicht allein. :-)
Properties: Attribute, die durch Get/Set-Methoden verwaltet werden
Properties sind eine clevere Möglichkeit, Attribute zu implementieren, deren *Verwendung* dem Attributzugriff ähnelt, deren *Implementierung* aber Methodenaufrufe verwendet. Diese werden manchmal als "verwaltete Attribute" bezeichnet. In früheren Python-Versionen konnte man dies nur durch Überschreiben von __getattr__ und __setattr__ tun; aber das Überschreiben von __setattr__ verlangsamt *alle* Attributzuweisungen erheblich, und das Überschreiben von __getattr__ ist immer etwas knifflig richtig hinzubekommen. Properties ermöglichen es Ihnen, dies schmerzfrei zu tun, ohne __getattr__ oder __setattr__ überschreiben zu müssen.
Ich zeige zuerst ein Beispiel. Definieren wir eine Klasse mit einem Attribut x, das durch ein Paar von Methoden, getx() und setx(), definiert ist
class C(object):
def __init__(self):
self.__x = 0
def getx(self):
return self.__x
def setx(self, x):
if x < 0: x = 0
self.__x = x
x = property(getx, setx)
Hier ist eine kleine Demonstration
>>> a = C()
>>> a.x = 10
>>> print a.x
10
>>> a.x = -10
>>> print a.x
0
>>> a.setx(12)
>>> print a.getx()
12
>>>
Die vollständige Signatur ist property(fget=None, fset=None, fdel=None, doc=None). Die Argumente fget, fset und fdel sind die Methoden, die aufgerufen werden, wenn das Attribut gelesen, gesetzt oder gelöscht wird. Wenn eine dieser drei nicht angegeben oder None ist, löst die entsprechende Operation eine AttributeError-Ausnahme aus. Das vierte Argument ist der Doc-String für das Attribut; er kann aus der Klasse wie im folgenden Beispiel gezeigt abgerufen werden
>>> class C(object):
... def getx(self): return 42
... x = property(getx, doc="hello")
...
>>> C.x.__doc__
'hello'
>>>
Dinge, die Sie über property() bemerken sollten (alles fortgeschrittenes Material außer dem ersten Punkt)
- Properties funktionieren nicht für klassische Klassen, aber Sie erhalten keine klare Fehlermeldung, wenn Sie dies versuchen. Ihre Get-Methode wird aufgerufen, so dass es zu funktionieren scheint, aber bei der Attributzuweisung setzt eine klassische Instanz die Klasse einfach den Wert in ihr __dict__, ohne die Set-Methode der Property aufzurufen, und danach wird auch die Get-Methode der Property nicht mehr aufgerufen. (Sie könnten __setattr__ überschreiben, um dies zu beheben, aber es wäre prohibitiv teuer.)
Was property() betrifft, so sind seine fget-, fset- und fdel-Argumente Funktionen, keine Methoden – sie erhalten als erstes Argument eine explizite Referenz auf das Objekt. Da property() typischerweise in einer Klassendefinition verwendet wird, ist dies korrekt (die Methoden sind tatsächlich *Funktions*objekte, zu dem Zeitpunkt, an dem property() aufgerufen wird), aber Sie können sie immer noch als Methoden betrachten – solange Sie keine Metaklasse verwenden, die spezielle Dinge mit Methoden tut.
- Die Get-Methode wird nicht aufgerufen, wenn auf die Property als Klassenattribut (C.x) statt als Instanzattribut (C().x) zugegriffen wird. Wenn Sie den __get__-Vorgang für Properties überschreiben möchten, wenn sie als Klassenattribut verwendet werden, können Sie property unterklassifizieren – es ist selbst ein New-Style-Typ –, um seine __get__-Methode zu erweitern, oder Sie können einen Deskriptortyp von Grund auf neu definieren, indem Sie eine New-Style-Klasse erstellen, die __get__, __set__ und __delete__ Methoden definiert.
Methodenauflösungsreihenfolge (Method Resolution Order)
Mit Mehrfachvererbung stellt sich die Frage nach der Methodenauflösungsreihenfolge: die Reihenfolge, in der eine Klasse und ihre Basen durchsucht werden, wenn nach einer Methode eines bestimmten Namens gesucht wird.
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):
"Look up name in cls and its base classes."
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
In Python 2.2 habe ich beschlossen, eine andere Suchregel für New-Style-Klassen zu übernehmen. (Die Regel für klassische Klassen bleibt aus Kompatibilitätsgründen unverändert; schließlich werden alle Klassen New-Style-Klassen sein und dann wird der Unterschied verschwinden.) Ich werde versuchen, zuerst zu erklären, was an der klassischen Regel falsch ist.
Das Problem mit der klassischen Regel 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 seinen Basistyp(en). Dieses spezielle Diagramm bedeutet, dass B und C von A erben, und D von B und C (und damit indirekt auch von A) erbt.
Nehmen wir an, C überschreibt die Methode save(), die in der Basis A definiert ist. (C.save() ruft wahrscheinlich A.save() auf und speichert dann einen Teil seines eigenen Zustands.) B und D überschreiben save() nicht. Wenn wir save() auf einer D-Instanz aufrufen, welche Methode wird dann aufgerufen? Gemäß der klassischen Suchregel wird A.save() aufgerufen und ignoriert C.save()!
Das ist nicht gut. Wahrscheinlich bricht es 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 Typ 'object' an der Spitze der Typenhierarchie definiert eine Reihe von Methoden, die von Subtypen nützlich erweitert werden können, z. B. __getattribute__() und __setattr__().
(Nebenbemerkung: Die __getattr__() Methode ist nicht wirklich die Implementierung für den Get-Attribut-Vorgang; sie ist ein Hook, der nur aufgerufen wird, wenn ein Attribut auf normale Weise nicht gefunden werden kann. Dies wurde oft als Mangel bemängelt – einige Klassendesigns haben einen legitimen Bedarf an einer Get-Attribut-Methode, die für *alle* Attributreferenzen aufgerufen wird, und dieses Problem wird jetzt durch die Verfügbarkeit von __getattribute__() gelöst. Aber dann muss diese Methode die Standardimplementierung irgendwie aufrufen können. Der natürlichste Weg ist, die Standardimplementierung als object.__getattribute__(self, name) verfügbar zu machen.)
Somit wird eine klassische Klassenhierarchie wie diese
class B class C:
^ ^ __setattr__()
\ /
\ /
\ /
\ /
class D
wird im neuen System zu einem Diamantdiagramm werden
object:
^ ^ __setattr__()
/ \
/ \
/ \
/ \
class B class C:
^ ^ __setattr__()
\ /
\ /
\ /
\ /
class D
und während im ursprünglichen Diagramm C.__setattr__() aufgerufen wird, würde im neuen System mit der klassischen Suchregel object.__setattr__() aufgerufen werden!
Glücklicherweise gibt es eine Suchregel, die besser ist. Sie ist etwas schwierig zu erklären, tut aber das Richtige im Diamantdiagramm und ist dieselbe wie die klassische 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 erfolgt, wenn die Klasse definiert wird, 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 die klassische Suche bei Vorhandensein von Diamanten 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. Das zweite Vorkommen ist redundant, da alles, was dort gefunden werden könnte, bereits gefunden worden wäre, als die erste Vorkommensweise durchsucht wurde. Aber es wird trotzdem besucht (die rekursive Implementierung der klassischen Regel merkt sich nicht, welche Klassen sie bereits besucht hat).
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 Vorfahrenklasse genau einmal (einschließlich der am höchsten abgeleiteten Klasse, D im Beispiel): D, B, C, A.
Die Suche nach Methoden in dieser Reihenfolge wird für das Diamantdiagramm das Richtige tun. Aufgrund der Art und Weise, wie die Liste konstruiert wird, ändert sich die Suchreihenfolge nie in Situationen, in denen kein Diamant beteiligt ist.
Ist das nicht rückwärts inkompatibel? Wird es bestehenden Code brechen? Es würde, wenn wir die Methodenauflösungsreihenfolge für alle Klassen ändern würden. Jedoch wird in Python 2.2 die neue Suchregel nur auf von integrierten Typen abgeleitete Typen angewendet, was ein neues Feature ist. Klassendefinitionen ohne Basisklasse erstellen "klassische Klassen", ebenso wie Klassendefinitionen, deren Basisklassen selbst klassische Klassen sind. Für klassische Klassen wird die klassische Suchregel verwendet. Wir könnten auch ein Werkzeug bereitstellen, das eine Klassenhierarchie analysiert und nach Methoden sucht, die von einer Änderung der Methodenauflösungsreihenfolge betroffen wären.
Ordnungswidersprüche
Dieser Abschnitt ist nur für fortgeschrittene Leser. Die aktuelle Implementierung verwendet einen subtil anderen Algorithmus, der in seltenen Fällen eine etwas andere Suchreihenfolge ergeben kann. Der Unterschied zeigt sich nur, wenn zwei gegebene Basisklassen in unterschiedlicher Reihenfolge in der Vererbungsliste zweier verschiedener abgeleiteter Klassen vorkommen und diese abgeleiteten Klassen wiederum von einer anderen Klasse geerbt werden. Hier ist das kleinste Beispiel, das mir einfällt class A(object):
def meth(self): return "A"
class B(object):
def meth(self): return "B"
class X(A, B): pass
class Y(B, A): pass
class Z(X, Y): pass
Gemäß dem oben genannten Algorithmus sollte Z's MRO (Method Resolution Order) [Z, X, Y, B, A, object] sein. Aber wenn Sie dies in Python 2.2 versuchen (unter Verwendung von Z.__mro__, siehe unten), erhalten Sie [Z, X, Y, A, B, object]! In einer zukünftigen Version könnten zwei Dinge passieren: Z's MRO könnte sich zu [Z, X, Y, B, A, object] ändern; oder die Deklaration der Klasse Z könnte illegal werden, da sie eine "Ordnungsabweichung" einführt: Klasse A geht B in der Vererbungsliste von X voraus, aber folgt ihr in der Vererbungsliste von Y.
Das Buch "Putting Metaclasses to Work", das mich dazu inspirierte, das MRO zu ändern, definiert den aktuell implementierten MRO-Algorithmus, aber seine Beschreibung des Algorithmus ist ziemlich schwer zu verstehen – ich habe nicht einmal gemerkt, dass der obige Algorithmus nicht immer dasselbe MRO berechnet, bis Tim Peters ein Gegenbeispiel fand. Glücklicherweise können Gegenbeispiele nur auftreten, wenn es Ordnungsabweichungen im Vererbungsgraphen gibt. Das Buch verbietet Klassen, die solche Ordnungsabweichungen enthalten, wenn die Ordnungsabweichung "ernst" ist. Eine Ordnungsabweichung zwischen zwei Klassen ist ernst, wenn die beiden Klassen mindestens eine Methode mit demselben Namen definieren. Im obigen Beispiel ist die Ordnungsabweichung ernst. In Python 2.2 habe ich mich entschieden, keine ernsten Ordnungsabweichungen zu prüfen; aber die Bedeutung eines Programms, das eine ernsthafte Ordnungsabweichung enthält, ist undefiniert, und seine Auswirkung kann sich in Zukunft ändern.
Kooperative Methoden und "super"
Eines der coolsten, aber vielleicht auch ungewöhnlichsten Features der neuen Klassen ist die Möglichkeit, "kooperative" Klassen zu schreiben. Kooperative Klassen werden mit Blick auf Mehrfachvererbung geschrieben, unter Verwendung eines Musters, das ich als "kooperativer Super-Aufruf" bezeichne. Dies ist in einigen anderen Mehrfachvererbungssprachen als "call-next-method" bekannt und ist leistungsfähiger als der Super-Aufruf, der in Single-Inheritance-Sprachen wie Java oder Smalltalk zu finden ist. C++ hat keine der beiden Formen eines Super-Aufrufs und verlässt sich stattdessen auf einen expliziten Mechanismus, der dem im klassischen Python verwendeten ähnelt. (Der Begriff "kooperative Methode" stammt aus "Putting Metaclasses to Work".)
Zur Auffrischung wollen wir zunächst den traditionellen, nicht-kooperativen Super-Aufruf überprüfen. Wenn eine Klasse C von einer Basisklasse B erbt, möchte C oft eine in B definierte Methode m überschreiben. Ein "Super-Aufruf" tritt auf, wenn die Definition von m in C die Definition von m in B aufruft, um einen Teil seiner Arbeit zu erledigen. In Java kann der Body von m in C super(a, b, c) schreiben, um die Definition von m in B mit der Argumentenliste (a, b, c) aufzurufen. In Python schreibt C.m B.m(self, a, b, c), um denselben Effekt zu erzielen. Zum Beispiel
class B:
def m(self):
print "B here"
class C(B):
def m(self):
print "C here"
B.m(self)
Wir sagen, dass die Methode m von C die Methode m von B "erweitert". Dieses Muster funktioniert gut, solange wir einfache Vererbung verwenden, aber es bricht bei Mehrfachvererbung zusammen. Betrachten wir vier Klassen, deren Vererbungsdiagramm eine "Raute" bildet (dieselbe Raute wurde im vorherigen Abschnitt grafisch gezeigt) class A(object): ..
class B(A): ...
class C(A): ...
class D(B, C): ...
Angenommen, A definiert eine Methode m, die sowohl von B als auch von C erweitert wird. Was soll D jetzt tun? Es erbt zwei Implementierungen von m, eine von B und eine von C. Traditionell wählt Python einfach die erste gefundene aus, in diesem Fall die Definition von B. Das ist nicht ideal, da es die Definition von C vollständig ignoriert. Um zu sehen, was falsch daran ist, C's m zu ignorieren, nehmen Sie an, dass diese Klassen eine Art Hierarchie persistenter Container darstellen und betrachten Sie eine Methode, die die Operation "speichern Sie Ihre Daten auf der Festplatte" implementiert. Vermutlich hat eine D-Instanz sowohl die Daten von B als auch die Daten von C, sowie die Daten von A (eine einzige Kopie der letzteren). Das Ignorieren der Definition der Speichermethode von C würde bedeuten, dass eine D-Instanz, wenn sie aufgefordert wird, sich selbst zu speichern, nur die A- und B-Teile ihrer Daten speichert, aber nicht den Teil ihrer Daten, der von Klasse C definiert wird!
C++ bemerkt, dass D zwei widersprüchliche Definitionen der Methode m erbt und gibt eine Fehlermeldung aus. Der Autor von D soll m dann überschreiben, um den Konflikt zu lösen. Aber was soll die Definition von m in D tun? Sie kann m von B aufrufen, gefolgt von m von C, aber da beide Definitionen die von A geerbte Definition von m aufrufen, wird m von A zweimal aufgerufen! Abhängig von den Details der Operation ist dies bestenfalls eine Ineffizienz (wenn m idempotent ist), schlimmstenfalls ein Fehler. Klassisches Python hat dasselbe Problem, außer dass es nicht einmal als Fehler betrachtet wird, zwei widersprüchliche Definitionen einer Methode zu erben: es wählt einfach die erste aus.
Die traditionelle Lösung für dieses Dilemma besteht darin, jede abgeleitete Definition von m in zwei Teile aufzuteilen: eine partielle Implementierung _m, die nur die für eine Klasse eindeutigen Daten speichert, und eine vollständige Implementierung m, die ihre eigene _m und die der Basisklasse(n) aufruft. Zum Beispiel
class A(object):
def m(self): "save A's data"
class B(A):
def _m(self): "save B's data"
def m(self): self._m(); A.m(self)
class C(A):
def _m(self): "save C's data"
def m(self): self._m(); A.m(self)
class D(B, C):
def _m(self): "save D's data"
def m(self): self._m(); B._m(self); C._m(self); A.m(self)
Dieses Muster hat mehrere Probleme. Erstens gibt es die Verbreitung zusätzlicher Methoden und Aufrufe. Aber vielleicht noch wichtiger ist, dass es eine unerwünschte Abhängigkeit in den abgeleiteten Klassen von Details des Abhängigkeitsgraphen ihrer Basisklassen schafft: die Existenz von A kann nicht länger als Implementierungsdetail von B und C betrachtet werden, da Klasse D davon wissen muss. Wenn wir in einer zukünftigen Version des Programms die Abhängigkeit von A von B und C entfernen wollen, betrifft dies auch abgeleitete Klassen wie D; ebenso, wenn wir eine weitere Basisklasse AA zu B und C hinzufügen wollen, müssen alle ihre abgeleiteten Klassen ebenfalls aktualisiert werden.
Das "call-next-method"-Muster löst dieses Problem schön in Kombination mit der neuen Methodeauflösungsreihenfolge. Hier ist wie
class A(object):
def m(self): "save A's data"
class B(A):
def m(self): "save B's data"; super(B, self).m()
class C(A):
def m(self): "save C's data"; super(C, self).m()
class D(B, C):
def m(self): "save D's data"; super(D, self).m()
Beachten Sie, dass das erste Argument für super immer die Klasse ist, in der es vorkommt; das zweite Argument ist immer self. Beachten Sie auch, dass self nicht in der Argumentenliste für m wiederholt wird.
Um zu erklären, wie super funktioniert, betrachten wir das MRO für jede dieser Klassen. Das MRO wird durch das Klassenattribut __mro__ gegeben
A.__mro__ == (A, object)
B.__mro__ == (B, A, object)
C.__mro__ == (C, A, object)
D.__mro__ == (D, B, C, A, object)
Der Ausdruck super(C, self).m sollte nur innerhalb der Implementierung der Methode m in Klasse C verwendet werden. Beachten Sie, dass self zwar eine Instanz von C ist, self.__class__ aber möglicherweise nicht C ist: es kann eine von C abgeleitete Klasse sein (zum Beispiel D). Der Ausdruck super(C, self).m durchsucht dann self.__class__.__mro__ (das MRO der Klasse, die zur Erzeugung der Instanz in self verwendet wurde) nach dem Auftreten von C und beginnt dann, *nach* diesem Punkt nach einer Implementierung der Methode m zu suchen.
Wenn self beispielsweise eine C-Instanz ist, findet super(C, self).m die Implementierung von m in A, ebenso wie super(B, self).m, wenn self eine B-Instanz ist. Aber betrachten wir jetzt eine D-Instanz. In D's m findet super(D, self).m() B.m(self), da B die erste Basisklasse nach D in D.__mro__ ist, die m definiert. Jetzt wird in B.m super(B, self).m() aufgerufen. Da self eine D-Instanz ist, ist das MRO (D, B, C, A, object) und die Klasse nach B ist C. Hier wird die Suche nach einer Definition von m fortgesetzt. Dies findet C.m, das aufgerufen wird und seinerseits super(C, self).m() aufruft. Unter Beibehaltung desselben MRO sehen wir, dass die Klasse nach C A ist, und somit wird A.m aufgerufen. Dies ist die ursprüngliche Definition von m, so dass an diesem Punkt kein Super-Aufruf erfolgt.
Beachten Sie, wie derselbe Super-Ausdruck je nach Klasse von self eine andere Klasse findet, die eine Methode implementiert! Dies ist der Kern des kooperativen Super-Mechanismus.
Der oben gezeigte Super-Aufruf ist etwas fehleranfällig: Es ist leicht, einen Super-Aufruf von einer Klasse in eine andere zu kopieren und einzufügen, wobei man vergisst, den Klassennamen auf die Zielklasse zu ändern, und dieser Fehler wird nicht erkannt, wenn beide Klassen Teil desselben Vererbungsgraphen sind. (Sie können sogar eine unendliche Rekursion verursachen, indem Sie versehentlich den Namen einer abgeleiteten Klasse der Klasse, die den Super-Aufruf enthält, angeben.) Es wäre schön, wenn wir die Klasse nicht explizit nennen müssten, aber das würde mehr Hilfe vom Python-Parser erfordern, als wir derzeit erhalten können. Ich hoffe, dies in einer zukünftigen Python-Version zu beheben, indem der Parser super erkennt.
In der Zwischenzeit gibt es einen Trick, den Sie anwenden können. Wir können eine Klassenvariable namens __super erstellen, die ein "Binding"-Verhalten hat. (Binding-Verhalten ist ein neues Konzept in Python 2.2, formalisiert aber ein bekanntes Konzept aus klassischem Python: die Transformation von einer ungebundenen Methode zu einer gebundenen Methode, wenn sie über den getattr-Zugriff auf eine Instanz zugegriffen wird. Sie wird durch die __get__-Methode implementiert, die oben besprochen wurde.) Hier ist ein einfaches Beispiel
class A:
def m(self): "save A's data"
class B(A):
def m(self): "save B's data"; self.__super.m()
B._B__super = super(B)
class C(A):
def m(self): "save C's data"; self.__super.m()
C._C__super = super(C)
class D(B, C):
def m(self): "save D's data"; self.__super.m()
D._D__super = super(D)
Ein Teil des Tricks liegt in der Verwendung des Namens __super, der (durch die Namensverschleierungs-Transformation) den Klassennamen enthält. Dies stellt sicher, dass self.__super in jeder Klasse etwas anderes bedeutet (solange sich die Klassennamen unterscheiden; leider ist es in Python möglich, den Namen einer Basisklasse für eine abgeleitete Klasse wiederzuverwenden). Ein weiterer Teil des Tricks ist, dass das eingebaute super mit einem einzigen Argument aufgerufen werden kann und dann eine ungebundene Version erstellt, die durch einen späteren Instanz-getattr-Zugriff gebunden werden kann.
Leider ist dieses Beispiel aus mehreren Gründen immer noch ziemlich hässlich: super erfordert, dass die Klasse übergeben wird, aber die Klasse ist erst nach Abschluss der Ausführung der Klassenanweisung verfügbar, sodass das __super-Klassenattribut außerhalb der Klasse zugewiesen werden muss. Außerhalb der Klasse funktioniert die Namensverschleierung nicht (schließlich ist sie als Datenschutzfunktion gedacht), sodass die Zuweisung den nicht verschleierten Namen verwenden muss. Glücklicherweise ist es möglich, eine Metaklasse zu schreiben, die ihren Klassen automatisch ein __super-Attribut hinzufügt; siehe das autosuper-Metaklassenbeispiel unten.
Beachten Sie, dass super(class, subclass) ebenfalls funktioniert; dies ist für __new__ und andere statische Methoden erforderlich.
Beispiel: Super in Python codieren.
Als Illustration der Leistungsfähigkeit des neuen Systems ist hier eine voll funktionsfähige Implementierung der eingebauten Klasse super() in reinem Python. Dies kann auch helfen, die Semantik von super() zu verdeutlichen, indem die Suche im Detail erläutert wird. Die Druckanweisung am Ende des folgenden Codes gibt "DCBA" aus.
class Super(object):
def __init__(self, type, obj=None):
self.__type__ = type
self.__obj__ = obj
def __get__(self, obj, type=None):
if self.__obj__ is None and obj is not None:
return Super(self.__type__, obj)
else:
return self
def __getattr__(self, attr):
if isinstance(self.__obj__, self.__type__):
starttype = self.__obj__.__class__
else:
starttype = self.__obj__
mro = iter(starttype.__mro__)
for cls in mro:
if cls is self.__type__:
break
# Note: mro is an iterator, so the second loop
# picks up where the first one left off!
for cls in mro:
if attr in cls.__dict__:
x = cls.__dict__[attr]
if hasattr(x, "__get__"):
x = x.__get__(self.__obj__)
return x
raise AttributeError, attr
class A(object):
def m(self):
return "A"
class B(A):
def m(self):
return "B" + Super(B, self).m()
class C(A):
def m(self):
return "C" + Super(C, self).m()
class D(C, B):
def m(self):
return "D" + Super(D, self).m()
print D().m() # "DCBA"
Überschreiben der __new__-Methode
Beim Unterklassenbildung von unveränderlichen eingebauten Typen wie Zahlen und Zeichenketten und gelegentlich in anderen Situationen ist die statische Methode __new__ hilfreich. __new__ ist der erste Schritt bei der Instanzkonstruktion und wird *vor* __init__ aufgerufen. Die __new__-Methode wird mit der Klasse als erstem Argument aufgerufen; ihre Aufgabe ist es, eine neue Instanz dieser Klasse zurückzugeben. Vergleichen Sie dies mit __init__: __init__ wird mit einer Instanz als erstem Argument aufgerufen und gibt nichts zurück; ihre Aufgabe ist es, die Instanz zu initialisieren. Es gibt Situationen, in denen eine neue Instanz ohne Aufruf von __init__ erstellt wird (z. B. wenn die Instanz aus einem Pickle geladen wird). Es gibt keine Möglichkeit, eine neue Instanz zu erstellen, ohne __new__ aufzurufen (obwohl man in einigen Fällen damit durchkommen kann, die __new__-Methode einer Basisklasse aufzurufen).
Zur Erinnerung: Sie erstellen Klasseninstanzen, indem Sie die Klasse aufrufen. Wenn die Klasse eine new-style-Klasse ist, passiert Folgendes, wenn sie aufgerufen wird. Zuerst wird die __new__-Methode der Klasse aufgerufen, wobei die Klasse selbst als erstes Argument übergeben wird, gefolgt von allen (positionellen sowie Schlüsselwort-) Argumenten, die vom ursprünglichen Aufruf empfangen wurden. Dies gibt eine neue Instanz zurück. Dann wird die __init__-Methode dieser Instanz aufgerufen, um sie weiter zu initialisieren. (Dies wird übrigens alles durch die __call__-Methode der Metaklasse gesteuert.)
Hier ist ein Beispiel für eine Unterklasse, die __new__ überschreibt – so würden Sie sie normalerweise verwenden.
>>> class inch(float):
... "Convert from inch to meter"
... def __new__(cls, arg=0.0):
... return float.__new__(cls, arg*0.0254)
...
>>> print inch(12)
0.3048
>>>
Diese Klasse ist nicht sehr nützlich (es ist nicht einmal der richtige Weg, Einheitenumrechnungen durchzuführen), aber sie zeigt, wie der Konstruktor eines unveränderlichen Typs erweitert wird. Wenn wir stattdessen __init__ überschrieben hätten, hätte es nicht funktioniert
>>> class inch(float):
... "THIS DOESN'T WORK!!!"
... def __init__(self, arg=0.0):
... float.__init__(self, arg*0.0254)
...
>>> print inch(12)
12.0
>>>
Die Version, die __init__ überschreibt, funktioniert nicht, weil die __init__-Methode des float-Typs eine No-Op ist: sie kehrt sofort zurück und ignoriert ihre Argumente.
All dies geschieht, damit unveränderliche Typen ihre Unveränderlichkeit bewahren können, während sie die Unterklassenbildung ermöglichen. Wenn der Wert eines Float-Objekts durch seine __init__-Methode initialisiert würde, könnten Sie den Wert eines bestehenden Float-Objekts ändern! Zum Beispiel würde dies funktionieren
>>> # THIS DOESN'T WORK!!!
>>> import math
>>> math.pi.__init__(3.0)
>>> print math.pi
3.0
>>>
Ich hätte dieses Problem auf andere Weise lösen können, z. B. durch Hinzufügen eines Flags "bereits initialisiert" oder indem ich __init__ nur auf Unterklasseninstanzen zulasse, aber diese Lösungen sind unelegant. Stattdessen habe ich __new__ hinzugefügt, was ein vollkommen allgemeiner Mechanismus ist, der von eingebauten und benutzerdefinierten Klassen für unveränderliche und veränderliche Objekte verwendet werden kann.
Hier sind einige Regeln für __new__
- __new__ ist eine statische Methode. Wenn Sie sie definieren, müssen Sie nicht (aber dürfen!) die Phrase "__new__ = staticmethod(__new__)" verwenden, da dies durch ihren Namen impliziert wird (sie wird vom Klassenkonstruktor besonders behandelt).
- Das erste Argument für __new__ muss eine Klasse sein; die restlichen Argumente sind die Argumente, wie sie vom Konstruktoraufruf gesehen werden.
- Eine __new__-Methode, die die __new__-Methode einer Basisklasse überschreibt, kann die __new__-Methode dieser Basisklasse aufrufen. Das erste Argument für den Aufruf der __new__-Methode der Basisklasse sollte die Klasse sein, die an die überschreibende __new__-Methode übergeben wird, nicht die Basisklasse; wenn Sie die Basisklasse übergeben würden, würden Sie eine Instanz der Basisklasse erhalten.
- Sofern Sie nicht die in den nächsten beiden Aufzählungszeichen beschriebenen Spiele spielen möchten, *muss* eine __new__-Methode die __new__-Methode ihrer Basisklasse aufrufen; das ist der einzige Weg, eine Instanz Ihres Objekts zu erstellen. Die Unterklassen-__new__ kann zwei Dinge tun, um das resultierende Objekt zu beeinflussen: unterschiedliche Argumente an die Basisklassen-__new__ übergeben und das resultierende Objekt nach seiner Erstellung modifizieren (z. B. um wesentliche Instanzvariablen zu initialisieren).
- __new__ muss ein Objekt zurückgeben. Es gibt nichts, das erfordert, dass es ein neues Objekt zurückgibt, das eine Instanz seines Klassenarguments ist, obwohl das die Konvention ist. Wenn Sie ein vorhandenes Objekt zurückgeben, ruft der Konstruktoraufruf immer noch dessen __init__-Methode auf. Wenn Sie ein Objekt einer anderen Klasse zurückgeben, wird *dessen* __init__-Methode aufgerufen. Wenn Sie vergessen, etwas zurückzugeben, gibt Python unglücklicherweise None zurück, und Ihr Aufrufer wird wahrscheinlich sehr verwirrt sein.
- Für unveränderliche Klassen kann Ihre __new__ eine zwischengespeicherte Referenz auf ein vorhandenes Objekt mit demselben Wert zurückgeben; dies tun die int-, str- und tuple-Typen für kleine Werte. Dies ist einer der Gründe, warum ihre __init__ nichts tut: zwischengespeicherte Objekte würden immer wieder neu initialisiert. (Der andere Grund ist, dass es für __init__ nichts mehr zu initialisieren gibt: __new__ gibt ein vollständig initialisiertes Objekt zurück.)
- Wenn Sie eine eingebaute unveränderliche Klasse unterklassieren und einen zusätzlichen veränderlichen Zustand hinzufügen möchten (vielleicht fügen Sie eine Standardkonvertierung in einen Zeichentyp hinzu), ist es am besten, den veränderlichen Zustand in der __init__-Methode zu initialisieren und __new__ unangetastet zu lassen.
- Wenn Sie die Signatur des Konstruktors ändern möchten, müssen Sie oft sowohl __new__ als auch __init__ überschreiben, um die neue Signatur zu akzeptieren. Die meisten eingebauten Typen ignorieren jedoch die Argumente der Methoden, die sie nicht verwenden; insbesondere haben die unveränderlichen Typen (int, long, float, complex, str, unicode und tuple) eine Dummy-__init__, während die veränderlichen Typen (dict, list, file und auch super, classmethod, staticmethod und property) eine Dummy-__new__ haben. Der eingebaute Typ 'object' hat eine Dummy-__new__ und eine Dummy-__init__ (die die anderen erben). Der eingebaute Typ 'type' ist in vielerlei Hinsicht speziell; siehe den Abschnitt über Metaklassen.
- (Dies hat nichts mit __new__ zu tun, ist aber trotzdem nützlich zu wissen.) Wenn Sie eine eingebaute Klasse unterklassieren, wird automatisch zusätzlicher Speicher für Instanzen hinzugefügt, um __dict__ und __weakrefs__ aufzunehmen. (Das __dict__ wird erst initialisiert, wenn Sie es verwenden, daher sollten Sie sich keine Sorgen um den Speicher machen, den ein leeres Wörterbuch für jede von Ihnen erstellte Instanz belegt.) Wenn Sie diesen zusätzlichen Speicher nicht benötigen, können Sie die Phrase "__slots__ = []" zu Ihrer Klasse hinzufügen. (Siehe oben für mehr über __slots__.)
- Fakt: __new__ ist eine statische Methode, keine Klassenmethode. Ich dachte ursprünglich, es müsste eine Klassenmethode sein, und deshalb habe ich die classmethod-Primitive hinzugefügt. Bedauerlicherweise funktionieren Upcalls bei Klassenmethoden in diesem Fall nicht richtig, daher musste ich sie zu einer statischen Methode mit einer expliziten Klasse als erstem Argument machen. Ironischerweise gibt es in der Python-Distribution keine bekannten Verwendungen mehr für Klassenmethoden (außer in der Testsuite). Ich könnte classmethod sogar in einer zukünftigen Version abschaffen, wenn keine gute Verwendung dafür gefunden wird!
Als weiteres Beispiel für __new__ hier eine Möglichkeit, das Singleton-Muster zu implementieren.
class Singleton(object):
def __new__(cls, *args, **kwds):
it = cls.__dict__.get("__it__")
if it is not None:
return it
cls.__it__ = it = object.__new__(cls)
it.init(*args, **kwds)
return it
def init(self, *args, **kwds):
pass
Um eine Singleton-Klasse zu erstellen, unterklassieren Sie von Singleton; jede Unterklasse hat eine einzige Instanz, egal wie oft ihr Konstruktor aufgerufen wird. Um die Instanz der Unterklasse weiter zu initialisieren, sollten Unterklassen 'init' anstelle von __init__ überschreiben – die __init__-Methode wird jedes Mal aufgerufen, wenn der Konstruktor aufgerufen wird. Zum Beispiel
>>> class MySingleton(Singleton):
... def init(self):
... print "calling init"
... def __init__(self):
... print "calling __init__"
...
>>> x = MySingleton()
calling init
calling __init__
>>> assert x.__class__ is MySingleton
>>> y = MySingleton()
calling __init__
>>> assert x is y
>>>
Metaklassen
In der Vergangenheit hat das Thema Metaklassen in Python Haare aufwirbeln und sogar Gehirne explodieren lassen (siehe zum Beispiel Metaclasses in Python 1.5). Glücklicherweise sind Metaklassen in Python 2.2 zugänglicher und weniger gefährlich.
Terminologisch ist eine Metaklasse einfach "die Klasse einer Klasse". Jede Klasse, deren Instanzen selbst Klassen sind, ist eine Metaklasse. Wenn wir über eine Instanz sprechen, die keine Klasse ist, ist die Metaklasse der Instanz die Klasse ihrer Klasse: per Definition ist die Metaklasse von x x.__class__.__class__. Aber wenn wir über eine Klasse C sprechen, beziehen wir uns oft auf ihre Metaklasse, wenn wir C.__class__ meinen (nicht C.__class__.__class__, was eine Meta-Metaklasse wäre; diese haben nicht viel Nutzen, obwohl wir sie nicht ausschließen).
Der eingebaute 'type' ist die gebräuchlichste Metaklasse; es ist die Metaklasse aller eingebauten Typen. Klassische Klassen verwenden eine andere Metaklasse: den Typ namens types.ClassType. Letzterer ist relativ uninteressant; er ist ein historisches Artefakt, das benötigt wird, um klassischen Klassen ihr klassisches Verhalten zu verleihen. Sie können nicht über x.__class__.__class__ auf die Metaklasse einer klassischen Instanz zugreifen; Sie müssen type(x.__class__) verwenden, da klassische Klassen das __class__-Attribut nicht für Klassen (nur für Instanzen) unterstützen.
Wenn eine Klassenanweisung ausgeführt wird, bestimmt der Interpreter zuerst die geeignete Metaklasse M und ruft dann M(name, bases, dict) auf. Dies geschieht alles am *Ende* der Klassenanweisung, nachdem der Body der Klasse (wo Methoden und Klassenvariablen definiert sind) bereits ausgeführt wurde. Die Argumente für M sind der Klassenname (ein String aus der Klassenanweisung), ein Tupel von Basisklassen (Ausdrücke, die zu Beginn der Klassenanweisung ausgewertet werden; dies ist () , wenn keine Basisklassen in der Klassenanweisung angegeben sind) und ein Wörterbuch, das die Methoden und Klassenvariablen enthält, die von der Klassenanweisung definiert wurden. Was immer dieser Aufruf M(name, bases, dict) zurückgibt, wird dann der Variable zugewiesen, die dem Klassennamen entspricht, und das ist alles, was mit der Klassenanweisung zu tun ist.
Wie wird M bestimmt?
- Wenn dict['__metaclass__'] existiert, wird es verwendet.
- Andernfalls, wenn mindestens eine Basisklasse vorhanden ist, wird ihre Metaklasse verwendet (dies sucht zuerst nach einem __class__-Attribut und wenn das nicht gefunden wird, verwendet es ihren Typ). (In klassischem Python gab es diesen Schritt auch, er wurde aber nur ausgeführt, wenn die Metaklasse aufrufbar war. Dies wurde der Don Beaudry-Hook genannt – möge er in Frieden ruhen.)
- Andernfalls, wenn eine globale Variable namens __metaclass__ vorhanden ist, wird sie verwendet.
- Andernfalls wird die klassische Metaklasse (types.ClassType) verwendet.
Die häufigsten Ergebnisse hier sind, dass M entweder types.ClassType (Erstellung einer klassischen Klasse) oder 'type' (Erstellung einer new-style-Klasse) ist. Andere häufige Ergebnisse sind ein benutzerdefinierter Erweiterungstyp (wie Jim Fultons ExtensionClass) oder ein Subtyp von 'type' (wenn wir new-style-Metaklassen verwenden). Aber es ist möglich, etwas völlig Ausgefallenes hier zu haben: Wenn wir eine Basisklasse angeben, die ein benutzerdefiniertes __class__-Attribut hat, können wir alles als "Metaklasse" verwenden. Das war das Gehirn-explodierende Thema meines ursprünglichen Metaklassen-Papiers, und ich werde es hier nicht wiederholen.
Es gibt immer eine zusätzliche Komplikation. Wenn Sie klassische Klassen und new-style-Klassen in der Liste der Basen mischen, wird die Metaklasse der ersten new-style-Basisklasse anstelle von types.ClassType verwendet (vorausgesetzt, dict['__metaclass__'] ist undefiniert). Der Effekt ist, dass, wenn Sie eine klassische und eine new-style-Klasse kreuzen, der Nachwuchs eine new-style-Klasse ist.
Und noch eine (ich verspreche, das ist die letzte Komplikation bei der Metaklassen-Bestimmung). Für new-style-Metaklassen gibt es die Einschränkung, dass die gewählte Metaklasse gleich oder eine Unterklasse jeder der Metaklassen der Basen ist. Betrachten Sie eine Klasse C mit zwei Basisklassen, B1 und B2. Sagen wir M = C.__class__, M1 = B1.__class__, M2 = B2.__class__. Dann fordern wir issubclass(M, M1) und issubclass(M, M2). (Das liegt daran, dass eine Methode von B1 eine Meta-Methode, die in M1 definiert ist, auf self.__class__ aufrufen können sollte, auch wenn self eine Instanz einer Unterklasse von B1 ist.)
Das Metaklassen-Buch beschreibt einen Mechanismus, bei dem eine geeignete Metaklasse automatisch erstellt wird, wenn nötig, durch Mehrfachvererbung von M1 und M2. In Python 2.2 habe ich einen einfacheren Ansatz gewählt, der eine Ausnahme auslöst, wenn die Metaklassen-Einschränkung nicht erfüllt ist; es liegt am Programmierer, eine geeignete Metaklasse über die Klassenvariable __metaclass__ bereitzustellen. Wenn jedoch eine der Basis-Metaklassen die Einschränkung erfüllt (einschließlich der explizit gegebenen __metaclass__, falls vorhanden), wird die erste gefundene Basis-Metaklasse, die die Einschränkung erfüllt, als Metaklasse verwendet.
In der Praxis bedeutet dies, dass Sie sich keine Sorgen um die Metaklassen-Einschränkung machen müssen, wenn Sie eine degenerierte Metaklassen-Hierarchie haben, die die Form eines Turms hat (d. h. für zwei Metaklassen M1 und M2 ist mindestens eine von issubclass(M1, M2) oder issubclass(M2, M1) immer wahr). Zum Beispiel
# Metaclasses
class M1(type): ...
class M2(M1): ...
class M3(M2): ...
class M4(type): ...
# Regular classes
class C1:
__metaclass__ = M1
class C2(C1):
__metaclass__ = M2
class C3(C1, C2):
__metaclass__ = M3
class D(C2, C3):
__metaclass__ = M1
class C4:
__metaclass__ = M4
class E(C3, C4):
pass
Für Klasse C2 ist die Einschränkung erfüllt, weil M2 eine Unterklasse von M1 ist. Für Klasse C3 ist sie erfüllt, weil M3 eine Unterklasse von sowohl M1 als auch M2 ist. Für Klasse D ist die explizite Metaklasse M1 keine Unterklasse der Basis-Metaklassen (M2, M3), aber die Wahl von M3 erfüllt die Einschränkung, also ist D.__class__ M3. Klasse E ist jedoch ein Fehler: die beiden beteiligten Metaklassen sind M3 und M4, und keine ist eine Unterklasse der anderen. Diesen letzten Fall können wir wie folgt beheben
# A new metaclass
class M5(M3, M4): pass
# Fixed class E
class E(C3, C4):
__metaclass__ = M5
(Der Ansatz aus dem Metaklassenbuch würde die Klassendefinition für M5 automatisch liefern, gegeben die ursprüngliche Definition von Klasse E.)
Metaklassen-Beispiele
Lassen Sie uns zuerst etwas Theorie auffrischen. Denken Sie daran, dass eine Klassenanweisung einen Aufruf an M(name, bases, dict) verursacht, wobei M die Metaklasse ist. Nun ist eine Metaklasse eine Klasse, und wir haben bereits festgestellt, dass, wenn eine Klasse aufgerufen wird, ihre __new__- und __init__-Methoden nacheinander aufgerufen werden. Daher wird etwas wie das Folgende passieren
cls = M.__new__(M, name, bases, dict)
assert cls.__class__ is M
M.__init__(cls, name, bases, dict)
Ich schreibe den __init__-Aufruf hier als Aufruf einer ungebundenen Methode. Dies verdeutlicht, dass wir die von M definierte __init__ aufrufen, nicht die in cls definierte __init__ (was die Initialisierung für Instanzen von cls wäre). Aber es ruft tatsächlich die __init__-Methode von Objekt cls auf; cls ist zufällig eine Klasse.
Unser erstes Beispiel ist eine Metaklasse, die die Methoden einer Klasse nach Methoden namens _get_<etwas> und _set_<etwas> durchsucht und automatisch Property-Deskriptoren namens <etwas> hinzufügt. Es stellt sich heraus, dass es ausreicht, __init__ zu überschreiben, um zu tun, was wir wollen. Der Algorithmus macht zwei Durchgänge: Zuerst sammelt er Namen von Properties, dann fügt er sie der Klasse hinzu. Der Sammeldurchlauf durchsucht dict, das ist das Wörterbuch, das die Klassenvariablen und -methoden darstellt (ausgenommen Variablen und Methoden der Basisklasse). Aber der zweite Durchlauf, der Property-Konstruktionsdurchlauf, sucht nach _get_<etwas> und _set_<etwas> als Klassenattributen. Das bedeutet, dass, wenn eine Basisklasse _get_x definiert und eine Unterklasse _set_x definiert, die Unterklasse eine Property x erhält, die aus beiden Methoden erstellt wird, obwohl nur _set_x in der Klasse der Unterklasse vorkommt. Somit können Sie Properties in einer Unterklasse erweitern. Beachten Sie, dass wir die Drei-Argumente-Form von getattr() verwenden, so dass ein fehlendes _get_x oder _set_x in None übersetzt wird und keinen AttributeError auslöst. Wir rufen auch die __init__-Methode der Basisklasse kooperativ mit super() auf.
class autoprop(type):
def __init__(cls, name, bases, dict):
super(autoprop, cls).__init__(name, bases, dict)
props = {}
for name in dict.keys():
if name.startswith("_get_") or name.startswith("_set_"):
props[name[5:]] = 1
for name in props.keys():
fget = getattr(cls, "_get_%s" % name, None)
fset = getattr(cls, "_set_%s" % name, None)
setattr(cls, name, property(fget, fset))
Testen wir autoprop mit einem albernen Beispiel. Hier ist eine Klasse, die ein Attribut x als seinen invertierten Wert unter self.__x speichert
class InvertedX:
__metaclass__ = autoprop
def _get_x(self):
return -self.__x
def _set_x(self, x):
self.__x = -x
a = InvertedX()
assert not hasattr(a, "x")
a.x = 12
assert a.x == 12
assert a._InvertedX__x == -12
Unser zweites Beispiel erstellt eine Klasse, 'autosuper', die eine private Klassenvariable namens __super hinzufügt, die auf den Wert super(cls) gesetzt ist. (Erinnern Sie sich an die Diskussion von self.__super oben.) Nun ist __super ein privater Name (beginnt mit zwei Unterstrichen), aber wir möchten, dass es ein privater Name der zu erstellenden Klasse ist, nicht ein privater Name von autosuper. Daher müssen wir die Namensverschleierung selbst durchführen und setattr() verwenden, um die Klassenvariable festzulegen. Für dieses Beispiel vereinfache ich die Namensverschleierung zu "fügen Sie einen Unterstrich und den Klassennamen ein". Wiederum reicht es aus, __init__ zu überschreiben, um zu tun, was wir wollen, und wieder rufen wir die __init__-Methode der Basisklasse kooperativ auf.
class autosuper(type):
def __init__(cls, name, bases, dict):
super(autosuper, cls).__init__(name, bases, dict)
setattr(cls, "_%s__super" % name, super(cls))
Testen wir nun autosuper mit der klassischen Diamantenraute
class A:
__metaclass__ = autosuper
def meth(self):
return "A"
class B(A):
def meth(self):
return "B" + self.__super.meth()
class C(A):
def meth(self):
return "C" + self.__super.meth()
class D(C, B):
def meth(self):
return "D" + self.__super.meth()
assert D().meth() == "DCBA"
(Unsere autosuper-Metaklasse wird leicht getäuscht, wenn Sie eine Unterklasse mit demselben Namen wie eine Basisklasse definieren; sie sollte diesen Zustand tatsächlich prüfen und einen Fehler auslösen, wenn er auftritt. Aber das ist mehr Code, als für ein Beispiel angemessen erscheint, daher überlasse ich es dem Leser als Übung.)
Nun haben wir zwei unabhängig entwickelte Metaklassen, wir können die beiden zu einer dritten Metaklasse kombinieren, die von beiden erbt
class autosuprop(autosuper, autoprop):
pass
Einfach, oder? Da wir beide Metaklassen kooperativ geschrieben haben (was bedeutet, dass ihre Methoden super() verwenden, um die Basisklassenmethode aufzurufen), ist das alles, was wir brauchen. Testen wir es
class A:
__metaclass__ = autosuprop
def _get_x(self):
return "A"
class B(A):
def _get_x(self):
return "B" + self.__super._get_x()
class C(A):
def _get_x(self):
return "C" + self.__super._get_x()
class D(C, B):
def _get_x(self):
return "D" + self.__super._get_x()
assert D().x == "DCBA"
Das war alles für heute. Ich hoffe, Ihr Gehirn tut nicht zu sehr weh!
Rückwärtsinkompatibilitäten
Entspannen Sie sich! Die meisten der oben beschriebenen Features werden nur aufgerufen, wenn Sie eine Klassenanweisung mit einem eingebauten Objekt als Basisklasse verwenden (oder wenn Sie eine explizite __metaclass__-Zuweisung verwenden).
Einige Dinge, die sich auf alten Code auswirken könnten
- Siehe auch die Liste der Fehler in 2.2.
- Introspektion funktioniert anders (siehe PEP 252). Insbesondere haben die meisten Objekte jetzt ein __class__-Attribut, und die __methods__- und __members__-Attribute funktionieren nicht mehr, und die dir()-Funktion funktioniert anders. Siehe auch oben.
- Mehrere eingebaute Funktionen, die als Koerzionen oder Konstruktoren angesehen werden können, sind jetzt Typobjekte anstelle von Factory-Funktionen; die Typobjekte unterstützen dieselben Verhaltensweisen wie die alten Factory-Funktionen. Betroffen sind: complex, float, long, int, str, tuple, list, unicode und type. (Es gibt auch neue: dict, object, classmethod, staticmethod, aber da dies neue eingebaute sind, kann ich nicht sehen, wie dies alten Code brechen würde.) Siehe auch oben.
- Es gibt einen sehr spezifischen (und glücklicherweise seltenen) Fehler, der früher unbemerkt blieb, der aber jetzt als Fehler gemeldet wird
class A: def foo(self): pass class B(A): pass class C(A): def foo(self): B.foo(self)Hier möchte C.foo A.foo aufrufen, ruft aber versehentlich B.foo auf. Im alten System, da B foo nicht definiert, ist B.foo identisch mit A.foo, also würde der Aufruf gelingen. Im neuen System ist B.foo als Methode markiert, die eine B-Instanz erfordert, und ein C ist kein B, also schlägt der Aufruf fehl. - Binäre Kompatibilität mit alten Erweiterungen wird nicht garantiert. Wir haben dies während des Alpha- und Beta-Release-Zyklus für Python 2.2 verschärft. Ab 2.2b1 funktioniert Jim Fultons ExtensionClass (wie ein Test von Zope 2.4 zeigt), und ich gehe davon aus, dass auch andere Erweiterungen, die auf dem Hook von Don Beaudry basieren, funktionieren werden. Obwohl das Endziel von PEP 253 darin besteht, ExtensionClass abzuschaffen, glaube ich, dass ExtensionClass in Python 2.2 immer noch funktionieren sollte und nicht früher als Python 2.3 gebrochen wird.
Zusätzliche Themen
Diese Themen sollten ebenfalls diskutiert werden
- Deskriptoren: __get__, __set__, __delete__
- Die Spezifikationen der integrierten Typen, die Unterklassen bilden können
- Der Typ 'object' und seine Methoden
- <type 'foo'> vs. <type 'mod.foo'> vs. <class 'mod.foo'>
- Was noch?
Referenzen
- PEP 252 - Typen Klassen ähnlicher machen
- PEP 253 - Unterklassen von integrierten Typen
- Metaklassen in Python 1.5 - Auch bekannt als Der Killerwitz
- Metaklassen in die Praxis umsetzen: Eine neue Dimension in der objektorientierten Programmierung, von Ira R. Forman und Scott H. Danforth. Addison-Wesley, 1999, ISBN 0-201-43305-2.
