Hinweis: Obwohl JavaScript für diese Website nicht unbedingt erforderlich ist, werden Ihre Interaktionsmöglichkeiten mit den Inhalten eingeschränkt sein. Bitte aktivieren Sie JavaScript für das volle Erlebnis.

Vereinheitlichung von Typen und Klassen in Python 2.2

Python-Version: 2.2.3

Guido van Rossum

Dieses Papier ist ein unvollständiger Entwurf. Ich bitte um Feedback. Wenn Sie Probleme feststellen, schreiben Sie mir bitte unter guido@python.org.

Inhaltsverzeichnis

Änderungsprotokoll

Änderungen seit der ursprünglichen Version Python 2.2 dieses Tutorials

  • Erschrecken Sie die Leute nicht, indem Sie andeuten, dass classmethod verschwinden könnte. (4. April 2002)

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 davon ist die Einschränkung, integrierte Typen (wie den Typ von Listen und Dictionaries) nicht als Basisklasse in einer Klassenanweisung zu verwenden.

Dies ist eine der größten Änderungen an Python überhaupt, und doch kann sie mit sehr wenigen Abwärtsinkompatibilitäten durchgeführt werden. Die Änderungen werden in kleinster Detailtiefe in einer Reihe von PEPs (Python Enhancement Proposals) beschrieben. PEPs sind nicht als Tutorials gedacht, 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 bezieht, die mit einer Klassenanweisung definiert wurden, die kein integriertes Objekt unter ihren Basen hat: entweder weil sie keine Basen hat oder weil alle ihre Basen selbst klassische Klassen sind - die Definition rekursiv anwenden.

Klassische Klassen sind in Python 2.2 immer noch eine spezielle Kategorie. Letztendlich werden sie vollständig mit Typen vereinheitlicht, aber aufgrund zusätzlicher Abwä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 ist, welche Interpretation gemeint ist, werde ich versuchen, explizit zu sein und "klassische Klasse" oder "Klasse oder Typ" zu verwenden.

Ableiten von integrierten Typen

Beginnen wir mit dem saftigsten Stück: Sie können integrierte Typen wie Dictionaries und Listen untertypen. Alles, was Sie brauchen, ist ein Name für eine Basisklasse, die ein integrierter Typ ist, und Sie sind im Geschäft.

Es gibt einen neuen integrierten Namen, "dict", für den Typ von Dictionaries. (In Version 2.2b1 und früher hieß dieser "dictionary"; während ich im Allgemeinen keine Abkürzungen mag, war "dictionary" einfach zu lang zum Tippen, und wir sagen schon seit Jahren "dict".)

Dies ist wirklich nur Zucker, da es bereits zwei andere Möglichkeiten gibt, diesen Typ zu benennen: type({}) und (nach dem Import des types-Moduls) types.DictType (und eine dritte, types.DictionaryType). Aber da Typen jetzt eine zentralere Rolle spielen, scheint es angebracht, integrierte Namen für die Typen zu haben, auf die Sie wahrscheinlich stoßen 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 ein paar Dinge. Die __init__() Methode erweitert die dict.__init__() Methode. Wie __init__() Methoden es 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 "key in dict"-Tests, 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, diese Version ist weniger effizient, da sie den Schlüssel doppelt nachschlägt. Die Ausnahme wäre, wenn wir erwarten, dass der angeforderte Schlüssel fast nie im Dictionary vorhanden ist: Dann ist die Einrichtung der try/except-Anweisung teurer als der fehlschlagende "key in self"-Test.

Um vollständig zu sein, sollte die get() Methode wahrscheinlich auch erweitert werden, damit sie denselben Standardwert wie __getitem__() verwendet

    def get(self, key, *args):
        if not args:
            args = (self.default,)
        return dict.get(self, key, *args)

(Obwohl diese Methode mit einer variablen Argumentenliste deklariert ist, sollte sie eigentlich nur mit einem oder zwei Argumenten aufgerufen werden; wenn mehr übergeben werden, löst der Aufruf der Basisklassenmethode eine TypeError-Ausnahme aus.)

Wir sind nicht darauf beschränkt, Methoden zu erweitern, die in der Basisklasse definiert sind. Hier ist eine nützliche Methode, die etwas Ähnliches wie update() tut, aber bestehende Werte beibehält, anstatt sie durch neue Werte 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üssel-Liste 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, reicht aus.

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 klassisch nur "echte" Dictionaries erlaubt, 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 gibt dies nicht 0.0 aus? 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äufigsten 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 die Verwendung eines separaten Dictionaries zum Speichern einer einzelnen Instanzvariable verdoppelt 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 Speicher Platz für genau diese im Instanz. Wenn __slots__ verwendet wird, können andere Instanzvariablen nicht 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 eine AttributeError aus. (Beachten Sie, dass in Python 2.2b2 und früher Slot-Variablen standardmäßig den Wert None hatten und das "Löschen" dieser Variablen diesen Standardwert wiederherstellt.)

  • Sie können kein Klassenattribut verwenden, um einen Standardwert für eine Instanzvariable zu definieren, die von __slots__ definiert wird. Die __slots__ Deklaration erstellt ein Klassenattribut, das einen Deskriptor für jeden Slot enthält, und die Zuweisung eines Klassenattributs mit einem Standardwert würde diesen Deskriptor überschreiben.

  • Es gibt keine Prüfung, um Namenskonflikte zwischen den in einer Klasse definierten Slots und den in ihren Basisklassen definierten Slots zu verhindern. Wenn eine Klasse einen Slot definiert, der auch in einer Basisklasse definiert ist, ist die Instanzvariable, die durch den Slot der Basisklasse definiert wird, unzugänglich (außer durch direktes Abrufen ihres Deskriptors von der Basisklasse; dies könnte verwendet werden, um sie umzubenennen). Dies macht die Bedeutung Ihres Programms undefiniert; eine Prüfung zur Verhinderung dessen kann in Zukunft hinzugefügt werden.

  • 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. Variablenlange integrierte Typen sind long, str und tuple.

  • Eine Klasse, die __slots__ verwendet, unterstützt keine schwachen Referenzen auf ihre Instanzen, es sei denn, einer der Strings in der __slots__-Liste ist "__weakref__". (In Python 2.3 wurde diese Funktion auf "__dict__" erweitert.)

  • Die __slots__ Variable muss keine Liste sein; jedes nicht-Zeichenketten-Objekt, das iterierbar ist, reicht aus, und die von der Iteration zurückgegebenen Werte 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, z. B. können die Dictionary-Werte verwendet werden, um den Typ einer Instanzvariable einzuschrä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 die Überladung von Operatoren im Allgemeinen genauso funktioniert wie bei klassischen Klassen, es einige Unterschiede gibt. (Der größte ist das Fehlen der Unterstützung für __coerce__; neuartige Klassen sollten immer die neuartige numerische API verwenden, die den anderen Operanden unverändert an die Methoden __add__ und __radd__ usw. übergibt.)

Es gibt eine neue Möglichkeit, den Attributzugriff zu überschreiben. Der __getattr__ Hook, falls definiert, funktioniert genauso wie bei klassischen Klassen: Er wird nur aufgerufen, wenn der reguläre Weg, das Attribut zu suchen, 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__ beachten Sie, dass es leicht ist, eine unendliche Rekursion zu verursachen: Wann immer __getattribute__ auf ein Attribut von self verweist (sogar self.__dict__!), wird es rekursiv aufgerufen. (Dies ist ähnlich wie __setattr__, das für alle Attributzuweisungen aufgerufen wird; __getattr__ kann auch darunter leiden, wenn es unvorsichtig geschrieben ist und auf ein nicht existierendes Attribut von self verweist.)

Der korrekte Weg, jedes Attribut von self innerhalb von __getattribute__ zu erhalten, ist der Aufruf der __getattribute__ Methode der Basisklasse, so 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__ (eigentlich dessen 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)

Eine Anmerkung zu __setattr__: manchmal werden Attribute nicht in self.__dict__ gespeichert (zum Beispiel bei Verwendung von __slots__ oder Eigenschaften 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 werden es vielleicht nützlich finden zu erkennen, dass diese Form der Untertypisierung in Python sehr ähnlich der einfachen Vererbung von Unterklassen in C++ implementiert ist, wobei __class__ die Rolle der vtable spielt.

Es gibt noch viel mehr, das 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 werde mit einer Liste von Vorbehalten enden

  • 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 Name, der den merkmalslosen Basistyp aller integrierten Typen unter dem neuen System benennt.

  • Bei 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 Bugs in 2.2.

Integrierte Typen als Factory-Funktionen

Der vorherige Abschnitt zeigte, dass eine Instanz des integrierten Subtyps defaultdict durch Aufrufen von defaultdict() erstellt werden kann. Dies ist zu erwarten, da dies auch für klassische Klassen funktioniert. Aber hier ist eine neue Funktion: integrierte Basistypen selbst können auch instanziiert werden, indem der Typ direkt aufgerufen wird.

Für mehrere integrierte Typen gibt es in klassischem Python bereits Factory-Funktionen, die nach dem Typ benannt sind, z. B. str() und int(). Ich habe diese integrierten Funktionen geändert, sodass sie jetzt Namen für die entsprechenden Typen sind. Obwohl sich dadurch der Typ dieser Namen von integrierter Funktion zu integriertem Typ ändert, erwarte ich nicht, dass dies Kompatibilitätsprobleme verursacht: Ich habe dafür gesorgt, dass die Typen mit genau denselben Argumentenlisten aufgerufen werden können wie die früheren Funktionen. (Sie können im Allgemeinen auch ohne Argumente aufgerufen werden, was ein Objekt mit einem geeigneten Standardwert wie Null oder leer ergibt; dies ist neu.)

Dies sind die betroffenen integrierten Funktionen

  • int([Zahl oder Zeichenkette[, Basiszahl]])
  • long([Zahl oder Zeichenkette])
  • float([Zahl oder Zeichenkette])
  • complex([Zahl oder Zeichenkette[, Imaginärzahl]])
  • str([Objekt])
  • unicode([Zeichenkette[, Kodierungszeichenkette]])
  • tuple([iterierbar])
  • list([iterierbar])
  • type(Objekt) oder type(NameZeichenkette, Basis-Tupel, Methoden-Dictionary)

Die Signatur von type() erfordert eine Erklärung: traditionell gibt type(x) den Typ des Objekts x zurück, und diese Verwendung wird weiterhin unterstützt. Jedoch ist type(name, bases, methods) 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 erwähnen, dass diese Signatur dieselbe ist wie die, die vom Don Beaudry Hook der Metaklassen-Berühmtheit verwendet wird.)

Es gibt auch ein paar neue integrierte Funktionen, die dem gleichen Muster folgen. Diese wurden oben beschrieben oder werden unten beschrieben

  • dict([Mapping-Objekt oder iterierbares Objekt]) - gibt ein neues Dictionary zurück; das optionale Argument muss entweder ein Mapping sein, dessen Elemente kopiert werden, oder eine Sequenz von 2-Tupeln (oder von Sequenzen der Länge 2), die die (Schlüssel, Wert)-Paare angeben, die in das neue Dictionary eingefügt werden sollen
  • object([...]) - gibt ein neues, merkmalsloses Objekt zurück; Argumente werden ignoriert
  • classmethod(Funktion) - siehe unten
  • staticmethod(Funktion) - siehe unten
  • super(Klasse oder Typ[, Instanz]) - siehe unten
  • property([fget[, fset[, fdel[, doc]]]]) - siehe unten

Der Zweck dieser Änderung ist zweifach. Erstens erleichtert dies die Verwendung jedes dieser Typen als Basisklasse in einer Klassenanweisung. Zweitens macht es das Testen auf einen bestimmten Typ etwas einfacher: 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 jetzt ein Tupel von Klassen oder Typen sein. Zum Beispiel gibt isinstance(x, (int, long)) wahr 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 Geschlechts. Dies haben wir nicht für issubclass() getan. (Noch. Es wurde in Python 2.3 für issubclass() getan.)

Introspektion von Instanzen integrierter Typen

Für Instanzen integrierter Typen (und für neuartige 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 aus 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 durch spezielle Notationen aufgerufen werden, wie __iadd__ (+=), __len__ (len), __ne__ (!=).

Mehr über die neue dir() Funktion

  • dir() auf einer Instanz (klassisch oder neuartig) 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 neuartig) zeigt den Inhalt des __dict__ der Klasse und all ihrer Basisklassen an. Es zeigt keine Klassenattribute an, 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 mehr Details; insbesondere für Objekte, die __dict__ oder __class__ überschreiben, werden diese beachtet, und aus Kompatibilitätsgründen werden __members__ und __methods__ beachtet, 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 es meistens nützlich innerhalb einer Unterklassenmethode, 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 der Attributzuweisung ist für neuartige 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 neuartigen 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 die Neugierigen: Es gibt zwei Gründe, warum die Änderung von integrierten 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, zurückgegriffen wird. Zweitens, wenn Python in eine andere Anwendung eingebettet ist, die mehrere Python-Interpreter erstellt, werden die integrierten Klassenobjekte (als statisch zugewiesene Datenstrukturen) zwischen allen Interpretern geteilt; daher könnte Code, der in einem Interpreter läuft, einen anderen Interpreter verwüsten, 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 einfach 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. Es wird kein "self" in der Definition von foo() deklariert, und es wird keine Instanz im Aufruf benötigt. Wenn eine Instanz verwendet wird, dient sie nur dazu, die Klasse zu finden, die die statische Methode definiert. Dies funktioniert für klassische und neue Klassen!

Die Zeile "foo = staticmethod(foo)" in der Klassenanweisung ist das entscheidende Element: Dies macht foo() zu einer statischen Methode. Die integrierte staticmethod() wickelt ihr Funktionsargument in eine spezielle Art von Deskriptor ein, dessen __get__() Methode die ursprüngliche Funktion unverändert zurückgibt.

Mehr über __get__ Methoden: in Python 2.2 wird die Magie 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 staticmethod-Objekte gibt die zugrunde liegende Funktion zurück. Wenn ein Klassenattribut keine __get__ Methode hat, wird es nie an eine Instanz gebunden, oder mit anderen Worten, 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 kein C++- oder Java-Äquivalent 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 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() letztendlich 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 fahren wir mit dem 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 sieht der Aufruf von C.foo() von E.foo() die Klasse C als erstes Argument, nicht die Klasse E. Dies ist zu erwarten, da der Aufruf die Klasse C angibt. Aber es betont den Unterschied zwischen diesen Klassenmethoden und Methoden, die in Metaklassen definiert sind, wo ein Aufruf 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. :-)

Eigenschaften: Attribute, die durch Get/Set-Methoden verwaltet werden

Eigenschaften sind eine clevere Möglichkeit, Attribute zu implementieren, deren *Verwendung* der Attributzugriff ähnelt, deren *Implementierung* aber Methodenaufrufe verwendet. Diese sind manchmal auch als "verwaltete Attribute" bekannt. 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, um es richtig zu machen. Eigenschaften ermöglichen es Ihnen, dies mühelos zu tun, ohne __getattr__ oder __setattr__ überschreiben zu müssen.

Ich werde zuerst ein Beispiel zeigen. Definieren wir eine Klasse mit einem Attribut x, das durch ein Paar von Methoden, getx() und setx(), definiert wird

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 wie im folgenden Beispiel gezeigt aus der Klasse abgerufen werden

>>> class C(object):
...     def getx(self): return 42
...     x = property(getx, doc="hello")
... 
>>> C.x.__doc__
'hello'
>>> 

Dinge, die Sie an property() bemerken sollten (alles fortgeschrittenes Material außer dem ersten)

  • Eigenschaften funktionieren nicht für klassische Klassen, aber Sie erhalten keine klare Fehlermeldung, wenn Sie dies versuchen. Ihre Get-Methode wird aufgerufen, sodass es zu funktionieren scheint, aber bei der Attributzuweisung setzt eine klassische Klasseninstanz den Wert einfach in ihr __dict__, ohne die Set-Methode der Eigenschaft aufzurufen, und danach wird auch die Get-Methode der Eigenschaft nicht aufgerufen. (Sie könnten __setattr__ überschreiben, um dies zu beheben, aber das wäre prohibitiv teuer.)

  • Was property() betrifft, so sind seine fget-, fset- und fdel-Argumente Funktionen, keine Methoden – sie werden mit einer expliziten Referenz auf das Objekt als erstes Argument übergeben. Da property() typischerweise in einer Klassenanweisung verwendet wird, ist dies korrekt (die Methoden sind tatsächlich Funktionsobjekte 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 die Eigenschaft als Klassenattribut (C.x) anstelle eines Instanzattributs (C().x) zugegriffen wird. Wenn Sie die __get__ Operation für Eigenschaften überschreiben möchten, wenn sie als Klassenattribut verwendet werden, können Sie property unterklassifizieren – es ist selbst ein neuartiger Typ –, um seine __get__ Methode zu erweitern, oder Sie können einen Deskriptortyp von Grund auf neu definieren, indem Sie eine neuartige Klasse erstellen, die __get__, __set__ und __delete__ Methoden definiert.

Methodenauflösungsreihenfolge

Mit Mehrfachvererbung kommt die Frage der Methodenauflösungsreihenfolge: die Reihenfolge, in der eine Klasse und ihre Basen bei der Suche nach einer Methode mit einem bestimmten Namen durchsucht werden.

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 neuartige Klassen zu übernehmen. (Die Regel für klassische Klassen bleibt aus Kompatibilitätsgründen unverändert; irgendwann werden alle Klassen neuartige Klassen sein und dann wird die Unterscheidung verschwinden.) Ich werde versuchen, zuerst zu erklären, was an der klassischen Regel falsch ist.

Das Problem mit der klassischen Regel wird deutlich, wenn wir ein "Diamantendiagramm" betrachten. Im Code

class A:
    def save(self): ...

class B(A):
    ...

class C(A):
    def save(self): ...

class D(B, C):
    ...
Oder als Diagramm mit Pfeilen, die Unterklassenbeziehungen darstellen (erklärt den Namen)
              class A:
                ^ ^  def save(self): ...
               /   \
              /     \
             /       \
            /         \
        class B     class C:
            ^         ^  def save(self): ...
             \       /
              \     /
               \   /
                \ /
              class D

Pfeile zeigen von einem Subtyp zu seinen Basistypen. Dieses spezielle Diagramm bedeutet, dass B und C von A abgeleitet sind und D von B und C (und damit auch indirekt von A) abgeleitet ist.

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, wobei C.save() ignoriert wird!

Das ist nicht gut. Es bricht wahrscheinlich C (sein Zustand wird nicht gespeichert), was den ganzen Sinn der Vererbung von C zunichtemacht.

Warum war das in klassischem Python kein Problem? Diamantendiagramme sind in klassischen Python-Klassen-Hierarchien selten zu finden. Die meisten Klassen-Hierarchien verwenden einfache Vererbung, und Mehrfachvererbung ist normalerweise auf Mix-in-Klassen beschränkt. Tatsächlich ist das hier gezeigte Problem wahrscheinlich der Grund, warum Mehrfachvererbung in klassischem Python unbeliebt ist!

Warum wird das im neuen System ein Problem sein? Der Typ 'object' am oberen Ende der Typ-Hierarchie 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 normalem Wege nicht gefunden werden kann. Dies wurde oft als Mangel angeführt – einige Klassendesigns haben einen legitimen Bedarf an einer Get-Attribut-Methode, die für *alle* Attributreferenzen aufgerufen wird, und dieses Problem ist jetzt gelöst, indem __getattribute__() verfügbar gemacht wird. 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 Klassen-Hierarchie wie diese

        class B     class C:
            ^         ^  __setattr__()
             \       /
              \     /
               \   /
                \ /
              class D

wird im neuen System zu einem Diamantendiagramm

              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, aber sie tut das Richtige im Diamantendiagramm 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 zuerst, wie eine solche Liste für die klassische Suchregel aussehen würde. Beachten Sie, dass bei Vorhandensein von Diamanten die klassische Suche einige Klassen mehrmals besucht. Zum Beispiel besucht die klassische Suchregel im obigen ABCD-Diamantendiagramm die Klassen in dieser Reihenfolge

D, B, A, C, A

Beachten Sie, wie A zweimal in der Liste vorkommt. Der zweite Eintrag ist redundant, da alles, was dort gefunden werden könnte, bereits beim Suchen des ersten Eintrags gefunden worden wäre. Aber er wird trotzdem besucht (die rekursive Implementierung der klassischen Regel erinnert sich nicht, welche Klassen sie bereits besucht hat).

Unter der neuen Regel wird die Liste sein

D, B, C, A

Die Suche nach Methoden in dieser Reihenfolge wird für das Diamantendiagramm 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.

Die genaue verwendete Regel wird im nächsten Abschnitt erklärt (der für die subtilsten Details auf ein separates Papier verweist). Ich bemerke hier nur die wichtige Eigenschaft der *Monotonie* in der Suchregel: Wenn Klasse X Klasse Y in der Suchreihenfolge für eine der Basisklassen von Klasse D vorangeht, dann wird Klasse X auch Klasse Y in der Suchreihenfolge für Klasse D vorangehen. Zum Beispiel geht B in der Suchliste für B A voran, also geht es auch in der Suchliste für D A voran; und dasselbe gilt für C, das A vorangeht. Ausnahme: Wenn unter den Basen von Klasse D eine ist, bei der X Y vorangeht, und eine andere, bei der Y X vorangeht, muss der Algorithmus einen Gleichstand auflösen. In diesem Fall sind alle Wetten ungültig; in Zukunft kann dieser Zustand eine Warnung oder einen Fehler verursachen.

(Eine Regel, die zuvor an dieser Stelle beschrieben wurde, hat nachweislich nicht die Monotonie-Eigenschaft. Siehe ein Thread auf python-dev, gestartet von Samuele Pedroni.)

Ist das nicht abwärtsinkompatibel? Wird das bestehenden Code brechen? Es würde, wenn wir die Methodenauflösungsreihenfolge für alle Klassen ändern würden. In Python 2.2 wird die neue Suchregel jedoch nur auf Typen angewendet, die von integrierten Typen abgeleitet sind, was eine neue Funktion ist. Klassenanweisungen ohne Basisklasse erstellen "klassische Klassen", ebenso wie Klassenanweisungen, deren Basisklassen selbst klassische Klassen sind. Für klassische Klassen wird die klassische Suchregel verwendet. Wir können auch ein Werkzeug bereitstellen, das eine Klassen-Hierarchie analysiert und nach Methoden sucht, die von einer Änderung der Methodenauflösungsreihenfolge betroffen wären.

Reihenfolgediskrepanzen und andere Anomalien

(Dieser Abschnitt ist nur für fortgeschrittene Leser.)

Jeder Algorithmus zur Bestimmung der Method Resolution Order (MRO) kann auf widersprüchliche Anforderungen stoßen. Dies zeigt sich beispielsweise, wenn zwei gegebene Basisklassen in unterschiedlicher Reihenfolge in der Vererbungsliste zweier verschiedener abgeleiteter Klassen vorkommen und diese abgeleiteten Klassen beide von einer weiteren Klasse geerbt werden. Hier ist ein Beispiel.

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

Wenn Sie dies versuchen (mit Z.__mro__, siehe unten), erhalten Sie [Z, X, Y, A, B, object], was die oben erwähnte Monotonieanforderung nicht erfüllt: Die MRO für Y ist [Y, B, A, object] und dies ist keine Teilsequenz der obigen Liste! Tatsächlich gibt es hier keine Lösung, die die Monotonieanforderung für beide X und Y erfüllt. Dies nennt man eine Reihenfolgedifferenz. In einer zukünftigen Version könnten wir entscheiden, solche Reihenfolgedifferenzen unter bestimmten Umständen zu verbieten oder Warnungen dafür auszugeben.

Das Buch "Putting Metaclasses to Work", das mich zu Änderungen an der MRO inspiriert hat, definiert den derzeit implementierten MRO-Algorithmus, aber seine Beschreibung des Algorithmus ist ziemlich schwer zu verstehen - ich hatte ursprünglich einen anderen, naiven Algorithmus dokumentiert und merkte nicht einmal, dass er nicht immer die gleiche MRO berechnete, bis Tim Peters ein Gegenbeispiel fand. Kürzlich hat Samuele Pedroni ein Gegenbeispiel gefunden, das zeigt, dass der naive Algorithmus die Monotonie nicht aufrechterhält, daher werde ich ihn nicht mehr beschreiben. Samuele hat mich überzeugt, einen neueren MRO-Algorithmus namens C3 zu verwenden, der im Paper "A Monotonic Superclass Linearization for Dylan" beschrieben wird. Dieser Algorithmus wird in Python 2.3 verwendet. C3 ist monoton wie der Algorithmus des Buches, behält aber zusätzlich die Reihenfolge der unmittelbaren Basisklassen bei, was der Algorithmus des Buches nicht immer tut. Eine sehr zugängliche Beschreibung von C3 für Python ist The Python 2.3 Method Resolution Order von Michele Simionato.

Das Buch verbietet Klassen, die solche Reihenfolgedifferenzen enthalten, wenn die Reihenfolgedifferenz "ernst" ist. Eine Reihenfolgedifferenz zwischen zwei Klassen ist ernst, wenn die beiden Klassen mindestens eine Methode mit demselben Namen definieren. Im obigen Beispiel ist die Reihenfolgedifferenz ernst. In Python 2.2 habe ich mich entschieden, keine ernsthaften Reihenfolgedifferenzen zu prüfen; aber die Bedeutung eines Programms, das eine ernsthafte Reihenfolgedifferenz enthält, ist undefiniert, und seine Auswirkung kann sich in Zukunft ändern. Aber seit Samueles Gegenbeispiel wissen wir, dass das Verbieten von Reihenfolgedifferenzen nicht ausreicht, um unterschiedliche Ergebnisse zwischen dem Python 2.2-Algorithmus (aus dem Buch) und dem Python 2.3-Algorithmus (C3, aus dem Dylan-Paper) zu vermeiden.

Kooperative Methoden und "super"

Eine der coolsten, aber vielleicht auch ungewöhnlichsten Funktionen der neuen Klassen ist die Möglichkeit, "kooperative" Klassen zu schreiben. Kooperative Klassen werden mit Mehrfachvererbung im Sinn geschrieben, unter Verwendung eines Musters, das ich als "kooperativen Super-Aufruf" bezeichne. Dies ist in einigen anderen Mehrfachvererbungssprachen als "call-next-method" bekannt und ist leistungsfähiger als der Super-Aufruf in Sprachen mit einfacher Vererbung wie Java oder Smalltalk. C++ hat keine Form des Super-Aufrufs und verlässt sich stattdessen auf einen expliziten Mechanismus, der dem in klassischem Python verwendeten ähnlich ist. (Der Begriff "kooperative Methode" stammt aus "Putting Metaclasses to Work".)

Zur Auffrischung, lassen Sie uns zuerst 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 Körper von m in C super(a, b, c) schreiben, um die Definition von m in B mit der Argumentliste (a, b, c) aufzurufen. In Python schreibt C.m B.m(self, a, b, c), um den gleichen 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 bei Mehrfachvererbung bricht es zusammen. Betrachten wir vier Klassen, deren Vererbungsdiagramm eine "Raute" bildet (dieses Diagramm wurde im vorherigen Abschnitt grafisch dargestellt)
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 nun 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, weil die Definition von C völlig ignoriert wird. Um zu sehen, was falsch daran ist, C's m zu ignorieren, nehmen wir an, dass diese Klassen eine Art persistente Containerhierarchie darstellen und betrachten eine Methode, die die Operation "Speichere deine Daten auf der Festplatte" implementiert. Vermutlich hat eine D-Instanz sowohl die Daten von B als auch die von C, sowie die von A (eine einzelne Kopie letzterer). Das Ignorieren der Definition von C für die save-Methode würde bedeuten, dass eine D-Instanz beim Auffordern zum Speichern nur die A- und B-Teile ihrer Daten speichert, aber nicht den Teil ihrer Daten, der von der 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 dann m überschreiben, um den Konflikt zu lösen. Aber was soll die Definition von m in D tun? Sie kann B's m gefolgt von C's m aufrufen, aber da beide Definitionen die von A geerbte Definition von m aufrufen, wird A's m zweimal aufgerufen! Abhängig von den Details der Operation ist dies im besten Fall eine Ineffizienz (wenn m idempotent ist), im schlimmsten Fall ein Fehler. Klassisches Python hat das gleiche Problem, außer dass es es nicht einmal als Fehler betrachtet, 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 zu zerlegen: eine partielle Implementierung _m, die nur die für eine Klasse eindeutigen Daten speichert, und eine vollständige Implementierung m, die ihr eigenes _m und das 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)

Es gibt mehrere Probleme mit diesem Muster. Erstens gibt es die Verbreitung zusätzlicher Methoden und Aufrufe. Aber vielleicht wichtiger ist, dass es eine unerwünschte Abhängigkeit in den abgeleiteten Klassen von den 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, wirkt sich dies auch auf abgeleitete Klassen wie D aus; ebenso müssen bei Hinzufügen einer weiteren Basisklasse AA zu B und C alle ihre abgeleiteten Klassen ebenfalls aktualisiert werden.

Das "call-next-method"-Muster löst dieses Problem schön in Kombination mit der neuen Method Resolution Order. 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 Argumentliste für m wiederholt wird.

Um zu erklären, wie super funktioniert, betrachten wir die MRO für jede dieser Klassen. Die MRO wird durch das __mro__ Klassenattribut 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 sucht dann in self.__class__.__mro__ (der MRO der Klasse, die zur Erstellung der Instanz in self verwendet wurde) nach dem Vorkommen von C und beginnt dann, nach einer Implementierung der Methode m *nach* diesem Punkt zu suchen.

Zum Beispiel, wenn self eine C-Instanz ist, findet super(C, self).m die Implementierung von m in A, genauso wie super(B, self).m, wenn self eine B-Instanz ist. Aber betrachten Sie jetzt eine D-Instanz. In D's m findet super(D, self).m() und ruft B.m(self) auf, da B die erste Basisklasse nach D in D.__mro__ ist, die m definiert. Nun wird in B.m super(B, self).m() aufgerufen. Da self eine D-Instanz ist, ist die 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 ruft seinerseits super(C, self).m() auf. Immer noch unter Verwendung derselben MRO sehen wir, dass die Klasse nach C A ist, und somit wird A.m aufgerufen. Dies ist die ursprüngliche Definition von m, daher wird an diesem Punkt kein Super-Aufruf gemacht.

Beachten Sie, wie derselbe Super-Ausdruck je nach Klasse von self eine andere Klasse findet, die eine Methode implementiert! Das 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 und dabei zu vergessen, den Klassennamen in den der Zielklasse zu ändern, und dieser Fehler wird nicht erkannt, wenn beide Klassen Teil desselben Vererbungsdiagramms sind. (Man kann sogar eine Endlosschleife verursachen, indem man versehentlich den Namen einer abgeleiteten Klasse der Klasse, die den Super-Aufruf enthält, übergibt.) 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 bekommen können. Ich hoffe, dies in einer zukünftigen Python-Version zu beheben, indem der Parser super erkennt.

In der Zwischenzeit gibt es hier 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 gut bekanntes Konzept aus klassischem Python: die Transformation einer ungebundenen Methode in eine gebundene Methode, wenn sie über den getattr-Zugriff auf eine Instanz abgerufen wird. Sie wird durch die __get__-Methode implementiert, die oben besprochen wird.) 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 Namensvermengungs-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 super-Built-in mit einem einzigen Argument aufgerufen werden kann, wodurch eine ungebundene Version erstellt wird, die durch eine spätere Instanz-getattr-Operation gebunden werden kann.

Leider ist dieses Beispiel aus einer Reihe von 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 Klassenerklärung verfügbar, so dass das __super-Klassenattribut außerhalb der Klasse zugewiesen werden muss. Außerhalb der Klasse funktioniert die Namensvermengung nicht (es ist ja auch ein Datenschutzmerkmal), daher muss die Zuweisung den unvermengten Namen verwenden. 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 Veranschaulichung der Leistungsfähigkeit des neuen Systems ist hier eine voll funktionsfähige Implementierung der Super()-Built-in-Klasse in reinem Python. Dies kann auch dazu beitragen, die Semantik von super() zu verdeutlichen, indem die Suche ausführlich dargelegt wird. Die print-Anweisung 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 Unterklassieren von unveränderlichen integrierten Typen wie Zahlen und Zeichenketten und gelegentlich in anderen Situationen ist die statische Methode __new__ sehr nützlich. __new__ ist der erste Schritt bei der Instanzkonstruktion, aufgerufen *bevor* __init__. Die Methode __new__ wird mit der Klasse als erstes 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 erstellt wird, ohne __init__ aufzurufen (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 davonkommt, die __new__ einer Basisklasse aufzurufen).

Zur Erinnerung, Sie erstellen Klasseninstanzen, indem Sie die Klasse aufrufen. Wenn die Klasse eine neuartige Klasse ist, geschieht 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ürde man es 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 für Einheitenumrechnungen), aber sie zeigt, wie der Konstruktor eines unveränderlichen Typs erweitert werden kann. Hätten wir stattdessen versucht, __init__ zu überschreiben, 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__ des float-Typs ein 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 und gleichzeitig Unterklassenbildung ermöglichen. Wenn der Wert eines Float-Objekts durch seine __init__-Methode initialisiert würde, könnten Sie den Wert eines vorhandenen 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 auch anders lösen können, zum Beispiel durch Hinzufügen eines "bereits initialisierten"-Flags oder indem ich __init__ nur für Unterklasseninstanzen aufrufe, aber diese Lösungen sind unelegant. Stattdessen habe ich __new__ hinzugefügt, was ein perfekt allgemeiner Mechanismus ist, der von integrierten 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. Bei ihrer Definition müssen Sie nicht (können aber!) den Ausdruck "__new__ = staticmethod(__new__)" verwenden, da dies durch ihren Namen impliziert ist (sie wird vom Klassenkonstruktor speziell 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 selbst; wenn Sie die Basisklasse übergeben würden, erhielten Sie eine Instanz der Basisklasse. (Dies ist wirklich nur analog zum Übergeben von self an einen überschriebenen __init__-Aufruf.)

  • Wenn Sie keine Spiele wie in den nächsten beiden Punkten beschriebenen 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: andere Argumente an die Basisklasse __new__ übergeben und das resultierende Objekt nach seiner Erstellung modifizieren (z. B. um wesentliche Instanzvariablen zu initialisieren).

  • __new__ muss ein Objekt zurückgeben. Nichts zwingt dazu, ein neues Objekt zurückzugeben, das eine Instanz ihres Klassenarguments ist, obwohl dies die Konvention ist. Wenn Sie ein vorhandenes Objekt ihrer Klasse oder einer Unterklasse zurückgeben, wird die __init__-Methode immer noch aufgerufen. Wenn Sie ein Objekt einer anderen Klasse zurückgeben, wird dessen __init__-Methode *nicht* aufgerufen. Wenn Sie vergessen, etwas zurückzugeben, gibt Python unheilvoll None zurück, und Ihr Aufrufer wird wahrscheinlich sehr verwirrt sein.

  • Für unveränderliche Klassen kann Ihr __new__ eine gecachte Referenz auf ein vorhandenes Objekt mit demselben Wert zurückgeben; das tun die int-, str- und tuple-Typen für kleine Werte. Dies ist einer der Gründe, warum ihre __init__ nichts tut: gecachte Objekte würden immer wieder neu initialisiert. (Der andere Grund ist, dass für __init__ nichts mehr zu initialisieren ist: __new__ gibt ein vollständig initialisiertes Objekt zurück.)

  • Wenn Sie einen integrierten unveränderlichen Typ unterklassieren und 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__ unverändert 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 integrierten Typen ignorieren jedoch die Argumente der Methoden, die sie nicht verwenden; insbesondere die unveränderlichen Typen (int, long, float, complex, str, unicode und tuple) haben eine Dummy-__init__, während die veränderlichen Typen (dict, list, file und auch super, classmethod, staticmethod und property) eine Dummy-__new__ haben. Der integrierte Typ 'object' hat eine Dummy-__new__ und eine Dummy-__init__ (die die anderen erben). Der integrierte Typ 'type' ist in vielerlei Hinsicht besonders; siehe den Abschnitt über Metaklassen.

  • (Das hat nichts mit __new__ zu tun, ist aber sowieso praktisch zu wissen.) Wenn Sie einen integrierten Typ unterklassieren, wird automatisch zusätzlicher Speicherplatz zu den Instanzen hinzugefügt, um __dict__ und __weakrefs__ aufzunehmen. (Das __dict__ wird jedoch erst initialisiert, wenn Sie es verwenden, daher sollten Sie sich keine Gedanken über den von einem leeren Wörterbuch für jede von Ihnen erstellte Instanz belegten Speicherplatz machen.) Wenn Sie diesen zusätzlichen Speicherplatz nicht benötigen, können Sie den Ausdruck "__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. Leider funktionieren Upcalls in diesem Fall mit Klassenmethoden nicht richtig, daher musste ich sie zu einer statischen Methode mit einer expliziten Klasse als erstes Argument machen. Ironischerweise gibt es jetzt keine bekannten Verwendungen für Klassenmethoden in der Python-Distribution (außer in der Test-Suite). Klassenmethoden sind jedoch an anderen Stellen nützlich, z. B. zur Programmierung von vererbbaren alternativen Konstruktoren.

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 Singleton; jede Unterklasse hat eine einzige Instanz, egal wie oft ihr Konstruktor aufgerufen wird. Um die Unterklasseninstanz 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 zu Berge stehen lassen und sogar Gehirne explodieren lassen (siehe z. B. 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 sind nicht sehr nützlich, obwohl wir sie nicht ausschließen).

Der eingebaute 'type' ist die häufigste Metaklasse; sie ist die Metaklasse aller eingebauten Typen. Klassische Klassen verwenden eine andere Metaklasse: den Typ namens types.ClassType. Letzterer ist relativ uninteressant; es ist ein historisches Artefakt, das benötigt wird, um klassischen Klassen ihr klassisches Verhalten zu verleihen. Sie können die Metaklasse einer klassischen Instanz nicht über x.__class__.__class__ abrufen; Sie müssen type(x.__class__) verwenden, da klassische Klassen das __class__-Attribut auf Klassen (nur auf Instanzen) nicht unterstützen.

Wenn eine Klassenerklärung 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 Klassenerklärung, nachdem der Körper der Klasse (wo Methoden und Klassenvariablen definiert sind) bereits ausgeführt wurde. Die Argumente für M sind der Klassenname (ein String aus der Klassenerklärung), ein Tupel von Basisklassen (Ausdrücke, die zu Beginn der Klassenerklärung ausgewertet werden; dies ist () wenn keine Basen in der Klassenerklärung angegeben sind) und ein Wörterbuch, das die von der Klassenerklärung definierten Methoden und Klassenvariablen enthält. Was auch immer dieser Aufruf M(name, bases, dict) zurückgibt, wird dann der Variablen zugewiesen, die dem Klassennamen entspricht, und das ist alles, was die Klassenerklärung ausmacht.

Wie wird M bestimmt?

  • Wenn dict['__metaclass__'] existiert, wird es verwendet.
  • Andernfalls wird, wenn mindestens eine Basisklasse vorhanden ist, deren Metaklasse verwendet (dies sucht zuerst nach einem __class__-Attribut und wenn dieses nicht gefunden wird, verwendet es seinen Typ). (In klassischem Python existierte dieser Schritt auch, wurde aber nur ausgeführt, wenn die Metaklasse aufrufbar war. Dies wurde als Don Beaudry Hook bezeichnet - möge er in Frieden ruhen.)
  • Andernfalls, wenn eine globale Variable namens __metaclass__ existiert, wird diese verwendet.
  • Andernfalls wird die klassische Metaklasse (types.ClassType) verwendet.

Die häufigsten Ergebnisse hier sind, dass M entweder types.ClassType (wodurch eine klassische Klasse erstellt wird) oder 'type' (wodurch eine neuartige Klasse erstellt wird) ist. Andere häufige Ergebnisse sind ein benutzerdefinierter Erweiterungstyp (wie Jim Fultons ExtensionClass) oder ein Subtyp von 'type' (wenn wir neuartige 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 gehirnsprengende Thema meines ursprünglichen Metaklassen-Papers, und ich werde es hier nicht wiederholen.

Es gibt immer eine zusätzliche Komplikation. Wenn Sie klassische Klassen und neuartige Klassen in der Liste der Basen mischen, wird die Metaklasse der ersten neuartigen Basisklasse anstelle von types.ClassType verwendet (vorausgesetzt, dict['__metaclass__'] ist undefiniert). Der Effekt ist, dass, wenn Sie eine klassische und eine neuartige Klasse kreuzen, der Nachkomme eine neuartige Klasse ist.

Und noch eine (ich verspreche, das ist die letzte Komplikation bei der Metaklassenbestimmung). Für neuartige Metaklassen gibt es die Einschränkung, dass die gewählte Metaklasse gleich jeder der Metaklassen der Basen ist oder eine Unterklasse davon 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). (Dies liegt daran, dass eine Methode von B1 eine Meta-Methode, die in M1 auf self.__class__ definiert ist, 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 Metaklassenbeschränkung nicht erfüllt ist; es liegt am Programmierer, eine geeignete Metaklasse über die __metaclass__-Klassenvariable bereitzustellen. Wenn jedoch eine der Basis-Metaklassen die Beschränkung erfüllt (einschließlich der explizit angegebenen __metaclass__, falls vorhanden), wird die erste gefundene Basis-Metaklasse, die die Beschränkung erfüllt, als Metaklasse verwendet.

In der Praxis bedeutet dies, dass Sie sich keine Sorgen über die Metaklassenbeschränkung machen müssen, wenn Sie eine degenerierte Metaklassenhierarchie haben, die die Form eines Turms hat (was bedeutet, dass für zwei Metaklassen M1 und M2 mindestens eine von issubclass(M1, M2) oder issubclass(M2, M1) immer wahr ist). 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 Beschränkung erfüllt, da M2 eine Unterklasse von M1 ist. Für Klasse C3 ist sie erfüllt, da M3 eine Unterklasse von beiden M1 und 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 Beschrä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. Wir können diesen letzteren Fall wie folgt korrigieren.

# A new metaclass
class M5(M3, M4): pass

# Fixed class E
class E(C3, C4):
    __metaclass__ = M5

(Der Ansatz aus dem Metaklassen-Buch würde die Klassendefinition für M5 angesichts der ursprünglichen Definition der Klasse E automatisch bereitstellen.)

Metaklassenbeispiele

Frischen wir zuerst etwas Theorie auf. Denken Sie daran, dass eine Klassenerklärung einen Aufruf von 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 geschieht etwas wie das hier.

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 nur zufällig eine Klasse.

Unser erstes Beispiel ist eine Metaklasse, die Methoden einer Klasse nach Methoden namens _get_ und _set_ durchsucht und automatisch Eigenschaftsdeskriptoren namens hinzufügt. Es stellt sich heraus, dass es ausreicht, __init__ zu überschreiben, um das zu tun, was wir wollen. Der Algorithmus macht zwei Durchläufe: Zuerst sammelt er Namen von Eigenschaften, dann fügt er sie zur Klasse hinzu. Der Sammeldurchlauf durchsucht dict, das ist das Wörterbuch, das die Klassenvariablen und -methoden darstellt (ohne Basisklassenvariablen und -methoden). Aber der zweite Durchlauf, der Eigenschaftskonstruktionsdurchlauf, sucht nach _get_ und _set_ als Klassenattributen. Das bedeutet, dass, wenn eine Basisklasse _get_x definiert und eine Unterklasse _set_x definiert, die Unterklasse eine Eigenschaft x erstellt, die aus beiden Methoden gebildet wird, obwohl nur _set_x im Wörterbuch der Unterklasse vorkommt. Somit können Sie Eigenschaften in einer Unterklasse erweitern. Beachten Sie, dass wir die Drei-Argument-Form von getattr() verwenden, sodass ein fehlendes _get_x oder _set_x in None übersetzt wird, anstatt einen AttributeError auszulösen. 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 member in dict.keys():
            if member.startswith("_get_") or member.startswith("_set_"):
		props[member[5:]] = 1
	for prop in props.keys():
            fget = getattr(cls, "_get_%s" % prop, None)
            fset = getattr(cls, "_set_%s" % prop, None)
            setattr(cls, prop, 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 wird. (Zur Erinnerung an die Diskussion von self.__super oben.) Nun ist __super ein privater Name (beginnt mit doppelter Unterstrich), aber wir wollen, dass es ein privater Name der zu erstellenden Klasse ist, nicht ein privater Name von autosuper. Daher müssen wir die Namensvermengung selbst durchführen und setattr() verwenden, um die Klassenvariable zu setzen. Für dieses Beispiel vereinfache ich die Namensvermengung zu "voranstellen eines Unterstrichs und des Klassennamens". Auch hier reicht es aus, __init__ zu überschreiben, um das zu tun, was wir wollen, und wir rufen 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 dem klassischen Rauten-Diagramm.

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 diese Bedingung tatsächlich prüfen und eine Ausnahme auslösen, wenn sie auftritt. Aber das ist mehr Code, als für ein Beispiel angemessen erscheint, daher lasse ich es als Übung für den Leser.)

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? Weil 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's für heute. Ich hoffe, Ihr Gehirn tut nicht zu sehr weh!

Abwärtsinkompatibilitäten

Entspannen Sie sich! Die meisten der oben beschriebenen Funktionen werden nur aufgerufen, wenn Sie eine Klassenerklärung mit einem integrierten 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 Attribute __methods__ und __members__ funktionieren nicht mehr, und die Funktion dir() funktioniert anders. Siehe auch oben.

  • Mehrere eingebaute Funktionen, die als Koerzionen oder Konstruktoren angesehen werden können, sind jetzt Typobjekte anstelle von Fabrikfunktionen; die Typobjekte unterstützen die gleichen Verhaltensweisen wie die alten Fabrikfunktionen. 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 Funktionen 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 unentdeckt 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 gelingt der Aufruf. Im neuen System ist B.foo als Methode markiert, die eine B-Instanz benötigt, und C ist keine 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 auf dem Don Beaudry Hook basierende Erweiterungen funktionieren werden. Während das Endziel von PEP 253 darin besteht, ExtensionClass abzuschaffen, glaube ich, dass ExtensionClass in Python 2.2 immer noch funktionieren sollte und es nicht früher als Python 2.3 bricht.

Zusätzliche Themen

Diese Themen sollten ebenfalls diskutiert werden

  • Deskriptoren: __get__, __set__, __delete__
  • Die Spezifikationen der eingebauten Typen, die unterklassierbar sind
  • Der 'object'-Typ und seine Methoden
  • <type 'foo'> vs. <type 'mod.foo'> vs. <class 'mod.foo'>
  • Was noch?

Referenzen