Magische Methoden und Operatorüberladung

Einführung

Marvin der Zauberer Die sogenannten magischen Methoden sind weder obskur noch haben sie etwas mit Magie zu tun. Wir haben bereits einige kennengelernt. Instanzen werden beispielsweise mit der __init__-Methode inititalisiert, nachdem eine andere Methode __new__, die man normalerweise nicht definieren muss, eine Instanz erzeugt hatte. Bei der Benutzung einer Klasse erzeugt man eine Instanz, indem man nur den Klassennamen mit einer gegebenenfalls leeren Paramterliste aufruft, also beispielsweise x = A(). Diese Anweisung wird automatisch (,,magisch'') von Python durch entsprechende Aufrufe von __new__ und __init__ ersetzt. Andere ,,magische'' Methoden, die wir bereits kennengelernt haben sind __str__ und __repr__. Die Parameterüberladung ist uns bereits häufig im Laufe dieses Buches begegnet, ohne dass es uns vielleicht bewusst war. Wir haben beispielsweise das Plus-Zeichen ,,+'' sowohl für die Addition von verschiedenen numerischen Werten, also Float- und Integerwerten, aber auch für die Konkatenation von Strings benutzt:
>>> a = 3 + 4
>>> a
7
>>> 3 + 5.543
8.543
>>> s = "Hello"
>>> print(s + " World")
Hello World
>>> 
Überladen des +-Operators Python erlaubt es auch, dass wir für eigene Klassen den ,,+''-Operator überladen können. Um dies tun zu können, müssen wir jedoch den internen Mechanismus verstehen, der die Überladung bewirkt. Für jedes Operatorzeichen gibt es eine spezielle Methode. Für ,,+'' lautet der Name der Methode beispielsweise __add__, und für ,,-'' lautet der Name __sub__. Bei __add__ und __sub__ handelt es sich um binäre Operatoren, weshalb die Methoden auch zwei Parameter benötigen: ,,self'' und ,,other''. Steht in einem Skript "x + y" und sind x und y von der Klasse K, dann ruft Python die Methode __add__ mit x.__add__(y) auf, falls es eine solche Methode in der Klasse K gibt, ansonsten erfolgt die Fehlermeldung

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'K' and 'K'


Übersicht über die magischen Methoden

Binäre Operatoren

Operator Methode
+ object.__add__(self, other)
- object.__sub__(self, other)
* object.__mul__(self, other)
// object.__floordiv__(self, other)
/ object.__div__(self, other)
% object.__mod__(self, other)
** object.__pow__(self, other[, modulo])
<< object.__lshift__(self, other)
>> object.__rshift__(self, other)
& object.__and__(self, other)
^ object.__xor__(self, other)
| object.__or__(self, other)

Erweiterte Zuweisungen

Operator Methode
+= object.__iadd__(self, other)
-= object.__isub__(self, other)
*= object.__imul__(self, other)
/= object.__idiv__(self, other)
//= object.__ifloordiv__(self, other)
%= object.__imod__(self, other)
**= object.__ipow__(self, other[, modulo])
<<= object.__ilshift__(self, other)
>>= object.__irshift__(self, other)
&= object.__iand__(self, other)
^= object.__ixor__(self, other)
|= object.__ior__(self, other)

Unäre Operatoren

Operator Methode
- object.__neg__(self)
+ object.__pos__(self)
abs() object.__abs__(self)
~ object.__invert__(self)
complex() object.__complex__(self)
int() object.__int__(self)
long() object.__long__(self)
float() object.__float__(self)
oct() object.__oct__(self)
hex() object.__hex__(self

Vergleichsoperatoren

Operator Methode
< object.__lt__(self, other)
<= object.__le__(self, other)
== object.__eq__(self, other)
!= object.__ne__(self, other)
>= object.__ge__(self, other)
> object.__gt__(self, other)


Beispielklasse: Length

In der folgenden Beispielklasse ,,Length'' wollen wir exemplarisch zeigen, wie man für eine eigene Klasse eine Addition mittels des ,,+''-Operators durch Überladung der __init__-Methode definieren kann. In der Klassendefinition befinden sich auch die magischen Methoden __str__ und __repr,__ die wir bereits zu Anfang des Kapitels besprochen hatten. Instanzen der Klasse Length sind Entfernungen. Eine Instanz beinhaltet die Länge der Strecke self.value und die Maßeinheit für diese Länge self.unit.

Die Klasse ermöglicht einem das einfache Ausrechnen von Ausdrücken der folgenden Art:

2.56 m + 3 yd + 7.8 in + 7.03 cm

Unter Benutzung unserer zu schreibenden Klasse sieht das dann so aus:

>>> from unit_conversions import Length
>>> L = Length
>>> print(L(2.56,"m") + L(3,"yd") + L(7.8,"in") + L(7.03,"cm"))
5.57162
>>> 

Die Klasse sieht wie folgt aus:

class Length:

    __metric = {"mm" : 0.001, 
                "cm" : 0.01,
                "m"  : 1,
                "km" : 1000,
                "in" : 0.0254,
                "ft" : 0.3048,
                "yd" : 0.9144,
                "mi" : 1609.344 }
    
    def __init__(self, value, unit = "m" ):
        self.value = value
        self.unit = unit
    
    def Converse2Metres(self):
        return self.value * Length.__metric[self.unit]
    
    def __add__(self, other):
        l = self.Converse2Metres() + other.Converse2Metres()
        return Length(l / Length.__metric[self.unit], self.unit )
    
    def __str__(self):
        return str(self.Converse2Metres())
    
    def __repr__(self):
        return str((self.value, self.unit))
    
if __name__ == "__main__":
    x = Length(4)
    print(x)
    print(repr(x))

    y = Length(4.5, "yd") + x
    print(repr(y))
    print(y) 


Starten wir das Programm erhalten wir folgende Ausgaben:

4
(4, 'm')
(8.87445319335083, 'yd')
8.114799999999999

Wir benötigen die Methode __iadd__, um die erweiterte Zuweisung zu implementieren:

    def __iadd__(self, other):
        l = self.Converse2Metres() + other.Converse2Metres()
        self.value = l / Length.__metric[self.unit]
        return self


Damit sind wir dann in der Lage, Zuweisungen der folgenden Art durchzuführen:

    x += Length(1)
    x += Length(4, "yd")

In der ersten erweiterten Zuweisung wird 1 Meter zu Length-Objekt x hinzugezählt, ohne dass wir die Einheit angeben müssen. Eigentlich wäre es doch schöner, wenn wir direkt eine Integer- oder eine Floatzahl zu einem Length-Objekt hinzuzählen könnten und unsere Implementierung würde diese Zahl gewissermaßen wie ein Length-Objekt behandeln. Dies lässt sich ganz einfach realisieren. Dazu ändern wir lediglich unser __add__-Methode so ab, dass sie den Typ von ,,other'' überprüft:

    def __add__(self, other):
        if type(other) == int or type(other) == float:
            l = self.Converse2Metres() + other
        else:
            l = self.Converse2Metres() + other.Converse2Metres()
        return Length(l / Length.__metric[self.unit], self.unit )

    def __iadd__(self, other):
        if type(other) == int or type(other) == float:
            l = self.Converse2Metres() + other
        else:
            l = self.Converse2Metres() + other.Converse2Metres()
        self.value = l / Length.__metric[self.unit]
        return self

Wenn man mit dieser Klasse eine Weile arbeitet, stellt sich mit Sicherheit eine neue Begehrlichkeit ein. Versucht man eine Integer- oder eine Floatzahl auf der linken Seite und ein Length-Objekt auf der rechten Seite bei der Addition zu verwenden erhält man eine Fehlermeldung:

>>> from unit_conversions import Length
>>> x = Length(3, "yd") + 5
>>> x = 5 + Length(3, "yd")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'Length'
>>> 


Auch für diese Problemstellung gibt es natürlich eine Lösung in Python und die liefert die Methode __radd__. Führt der Aufruf von int.__add__(5,Length(3, "yd")) zu einer Ausnahme, sucht Python nach der Methode __radd__, da unser zweiter Parameter (Length(3, "yd")) eine Instanz der Klasse Length ist. Existiert diese Methode, wird sie mit Length.__radd__(Length(3, "yd"), 5) aufgerufen, was dem Aufruf 5 + Length(3, "yd") entspricht.

Damit sieht eine Implementierung von __radd__ exakt gleich aus wie __add__:

    def __radd__(self, other):
        if type(other) == int or type(other) == float:
            l = self.Converse2Metres() + other
        else:
            l = self.Converse2Metres() + other.Converse2Metres()
        return Length(l / Length.__metric[self.unit], self.unit )

Es empfiehlt sich deshalb die Implementierung von __radd__ auf einen Aufruf von __add__ zu reduzieren:

    def __radd__(self, other):
        return Length.__add__(self,other)  

Im folgenden Diagramm veranschaulichen wir den Zusammenhang zwischen __add__ und __radd__:

Zusammenhang zwischen __add__ und __radd__

Standardklassen als Basisklassen

Statt selbstdefinierter Klassen kann man auch Standardklassen wie beispielsweise int, float, dict oder list als Basisklasse verwenden, um neue Klassen abzuleiten. Man kann beispielsweise zusätzliche Methoden für Standardklassen definieren.

Die Verarbeitung von Stapelspeichern (auch Kellerspeicher genannt, im Englischen stack) wird in Python, wie wir gesehen haben, mittels append und pop realisiert. Üblicherweise, d.h. in anderen Programmiersprachen, werden meistens die Funktionen push (deutsch: einkellern) und pop (deutsch: auskellern) verwendet.

Wir wollen im folgenden Beispiel die Klasse ,,list'' um eine Methode ,,push'' erweitern, die genau das Gleiche macht wie append:
class Plist(list):

    def __init__(self, l):
        list.__init__(self, l)

    def push(self, item):
        self.append(item)


if __name__ == "__main__":
    x = Plist([3,4])
    x.push(47)
    print(x)