· 

Magische Methoden in Python (Operatoren überladen)

1. Einführung

Für viele ist die Programmierung an sich bereits Magie in ihrer Reinform. Wenn man dann noch erfährt, dass es z. B. in Python sogenannte "Magische Methoden" gibt, dann erhärtet sich bei vielen vielleicht der Verdacht, dass beim Programmieren wirklich Zauberei im Spiel ist.  Diese magischen Methoden sind in Python die Grundlage für das polymorphe Verhalten von Operatoren. Was ist damit gemeint? Nun, dass eigentlich bekannte Operatoren (wie z. B. \(+\), \(-\), \(\cdot\) und \(\div\)) je nach Objekttyp unterschiedliche Funktionalitäten haben können. Man könnte mit einem Plus etwa eine reine numerische Addition oder bei Strings eine Konkatenation (also ein Hintereinanderhängen von Zeichenketten) durchführen. Magische Methoden erkennt man in Python daran, dass vor und hinter dem Methodennamen zwei Unterstriche stehen.
__methodenname__
Als Parameter wird immer eine self-Instanz übergeben. Weitere Parameter sind (je nach magischer Funktion) optional. Bei binären Operatoren taucht bspw. stets eine other-Instanz auf, also ein zweiter Operand, der für eine bestimmte Operation (z. B. die Addition) benötigt wird. Dazu später mehr.

2. Eine kleine Vektorklasse

Um zu demonstrieren, wie man mithilfe der magischen Methoden Operatoren überladen kann, schreiben wir uns eine kleine Vektorklasse, in der Vektoroperationen wie die Addition, Subtraktion oder das Skalarprodukt mithilfe der Operatoren \(+,-\) und \(*\) durchgeführt werden können. Auch andere Operationen wie z. B. die Berechnung des Betrags oder das "Umkehren" (bzw. Negieren) eines Vektors können in einer eigenen Vektorklasse umgesetzt werden.
Wir betrachten dabei Vektoren im \(\mathbb{R}^3\), d. h. $$\left(\begin{matrix}x\\y\\z\end{matrix}\right)$$ mit \(x,y,z\in\mathbb{R}\). Um einen Vektor in Python darstellen zu können, werden also drei Variablen benötigt. Zuerst wird die Bibliothek math importiert, um spezielle Funktionen wie z. B. das Wurzelziehen mit math.sqrt(...) zu ermöglichen.
import math
Als nächstes wird mit dem Keyword class die Vektorklasse gestartet.
import math

""" Vektorklasse """
class vector:
Im Konstruktor werden (neben der self-Instanz) nacheinander die Komponenten \(x,y,z\) eingegeben.
import math

""" Vektorklasse """
class vector:

        """ Konstruktor"""
        def __init__(self, x, y, z):
                self.x = x
                self.y = y
                self.z = z
Damit sind nun der Rahmen geschaffen, um die bereits angekündigten Funktionalitäten schrittweise durch Operator Overloading zu implementieren. Vektoren können nun wie folgt erzeugt werden:
# Erzeuge den Vektor (1,2,0)
v1 = vector(1,2,0)

3. Operatoren überladen

3.1 Ausgabeoperator überladen

Bevor man sich mit der Implementierung der Arithmetik beschäftigt, ist es sinnvoll, eine Ausgabefunktion zu schreiben, damit der Aufruf
print(vector(1,2,0))
eine menschenlesbare Darstellung des Vektors erzeugt und spätere Funktionen getestet werden können. Hierfür gibt es die magische Funktion
__str__(self)
Wenn man auf der Konsole die Ausgabe \((1,2,0)\) für den Vektor \(\left(\begin{matrix}1\\2\\0\end{matrix}\right)\) haben möchte, konkateniert man kommasepariert innerhalb des Methodenrumpfs die Werte für \(x,y,z\) und umschließt diese mit Klammern. self ist dabei eine Referenz auf den Vektor, der ausgegeben werden soll. Die Funktion str wird genutzt, um die Zahlenwerte in Strings umzuwandeln.
""" Ausgabe """
def __str__(self):
        return "(" + str(self.x) + "," + str(self.y) + "," + str(self.z) + ")"

3.2 Additionsoperator überladen

Zwei Vektoren werden addiert, indem man ihre \(x-,y-\) und \(z-\)Komponenten paarweise addiert: $$\left(\begin{matrix}x_1\\y_1\\z_1\end{matrix}\right) + \left(\begin{matrix}x_2\\y_2\\z_2\end{matrix}\right) = \left(\begin{matrix}x_1+x_2\\y_1+y_2\\z_1+z_2\end{matrix}\right)$$ Die Addition ist ein binärer Operator. Ihm werden zwei Instanzen übergeben, nämlich self und other. self entspricht dem ersten Summanden und other dem zweiten Summanden. Innerhalb der magischen Methode
__add__(self, other)
werden von self und other die \(x-,y-\) und \(z-\)Komponenten paarweise addiert (so, wie es das mathematische Vorbild verlangt).
""" Addition + """
def __add__(self, other):
        return vector(self.x + other.x, self.y + other.y, self.z + other.z)
Nun können zwei Vektoren definiert und sehr intuitiv addiert werden:
v1 = vector(2,2,0)
v2 = vector(1,0,1)

print(v1+v2) # Liefert (3,2,1)
Wenn nichts anderes angegeben wurde, funktioniert der Operator \(+=\) implizit.
v1 = vector(2,2,0)
v2 = vector(1,0,1)

v1 += v2

print(v1) # Liefert (3,2,1)
Man kann diesen über die magische Methode
__iadd__(self, other)
aber auch mit einer neuen Funktionalität versehen.

3.3 Subtraktionsoperator überladen

Zwei Vektoren werden subtrahiert, indem man ihre \(x-,y-\) und \(z-\)Komponenten paarweise subtrahiert: $$\left(\begin{matrix}x_1\\y_1\\z_1\end{matrix}\right) - \left(\begin{matrix}x_2\\y_2\\z_2\end{matrix}\right) = \left(\begin{matrix}x_1-x_2\\y_1-y_2\\z_1-z_2\end{matrix}\right)$$ Die Subtraktion ist ein binärer Operator. Ihm werden zwei Instanzen übergeben, nämlich self und other. self entspricht dem Subtrahenden und other dem Minuenden. Innerhalb der magischen Methode
__sub__(self, other)
werden von self und other die \(x-,y-\) und \(z-\)Komponenten paarweise subtrahiert (so, wie es das mathematische Vorbild verlangt).
""" Subtraktion - """
def __sub__(self, other):
        return vector(self.x - other.x, self.y - other.y, self.z - other.z)
Nun können zwei Vektoren definiert und sehr intuitiv subtrahiert werden:
v1 = vector(2,2,0)
v2 = vector(1,0,1)

print(v1-v2) # Liefert (1,2,-1)
Wenn nichts anderes angegeben wurde, funktioniert der Operator \(-=\) implizit.
v1 = vector(2,2,0)
v2 = vector(1,0,1)

v1 -= v2

print(v1) # Liefert (1,2,-1)
Man kann diesen über die magische Methode
__isub__(self, other)
aber auch mit einer neuen Funktionalität versehen.

3.4 Multiplikationsoperator überladen

Wenn man zwei Vektoren miteinander multiplizieren möchte, dann berechnet man damit das sog. Skalarprodukt. Dieses ist wie folgt definiert: $$\left(\begin{matrix}x_1\\y_1\\z_1\end{matrix}\right)\cdot \left(\begin{matrix}x_2\\y_2\\z_2\end{matrix}\right)= x_1\cdot x_2+y_1\cdot y_2+z_1\cdot z_2$$ Die einzelnen Komponenten der beiden Vektoren werden paarweise multipliziert und zum Schluss die Ergebnisse addiert. Beim Skalarprodukt kommt also kein Vektor heraus, sondern ein Skalar (eine Zahl).
Für die magische Methode
__mul__(self, other)
die den \(*-\)Operator überlädt, ändert sich jedoch nicht viel. Hier werden (dem mathematischen Vorbild folgend) die einzelnen Komponenten paarweise miteinander multipliziert, die Ergebnisse addiert und die Summe der Faktoren dann zurückgegeben:
""" Skalarprodukt * """
def __mul__(self, other):
        return self.x * other.x + self.y * other.y + self.z * other.z
Nun kann das Skalarprodukt zweier Vektoren sehr intuitiv mit dem Multiplikationsoperator berechnet werden:
v1 = vector(2,2,0)
v2 = vector(1,0,1)

print(v1*v2) # Liefert 2

3.5 Negationsoperator überladen

Wenn du die Richtung eines Vektors umkehren willst, dann multiplizierst du ihn mit dem Faktor \(-1\). $$-1\cdot \left(\begin{matrix}x\\y\\z\end{matrix}\right)= \left(\begin{matrix}-x\\-y\\-z\end{matrix}\right)$$
Auch wenn du den Subtraktionsoperator bereits überladen hast, ist Python (noch) nicht in der Lage, ein vor einen Vektor vorangestelltes Minus zu verarbeiten. Das liegt daran, dass dieses vorangestellte Minus ein eigener Operator ist, nämlich der für die Negation. Hierbei handelt es sich um einen sog. unären Operator, da kein weiterer Operand (wie z. B. bei der Addition oder Subtraktion) nötig.
Die magische Methode, die für den Negationsoperator verwendet wird, lautet
__neg__(self)
Im Gegensatz zu den bisherigen magischen Funktionen wird hier nur ein Parameter übergeben, nämlich die self-Instanz. Das liegt daran, dass der Negationsoperator (wie bereits erwähnt) ein unärer Operator ist. Um also die mathematische Richtungsumkehr zu implementieren, muss lediglich jede Komponente der self-Instanz negiert und der so gebildete Vektor zurückgegeben werden:
""" Negation """
def __neg__(self):
        return vector(-self.x, -self.y, -self.z)
Der Aufruf im Programm sieht dann wie folgt aus:
v1 = vector(2,2,0)

print(-v1) # Liefert (-2,-2,0)

3.6 Betrag eines Vektors berechnen

Den Betrag eines Vektors wird berechnet, indem man jede Komponente des Vektors quadriert, alle Komponenten addiert und aus der Summe anschließend die Quadratwurzel zieht. $$|v|=\sqrt{x^2+y^2+z^2}$$ Und genau dieser Logik folgend wird auch die Funktionalität innerhalb der magischen Methode
__abs__(self)
implementiert.
Diese besitzt nur einen Übergabeparameter, nämlich die self-Instanz. Auf dieser werden dann nacheinander die Werte für \(x,y\) und \(z\) aufgerufen, quadriert, addiert und anschließend die Wurzel gezogen.
""" Betrag """
def __abs__(self):
        return math.sqrt(self.x**2 + self.y**2 + self.z**2) 
Der Betrag eines Vektors lässt sich nun via Aufruf der Methode abs(...) berechnen.
v1 = vector(2,2,1)

print(abs(v1)) # Liefert 3.0

3.7 Äquivalenzoperator überladen

Um zu überprüfen, ob zwei Vektoren gleich sind, wäre es schön, wenn man den Vergleichsoperator \(==\) verwenden könnte. Um das zu bewerkstelligen, muss die magische Methode
__eq__(self, other)
imeplementiert werden. Da es sich um einen logischen Operator handelt, wird als Ergebnis ein boolean erwartet (also True oder False). Vergleiche also alle Komponenten der self-Instanz paarweise mit der entsprechenden Komponente der other-Instanz via \(==\) und verknüpfe sie mit dem logischen Und:
""" Auf Gleichheit prüfen """
def __eq__(self, other):
        return self.x == other.x and self.y == other.y and self.z == other.z
Nun kannst du zwei Vektoren miteinander vergleichen.
v1 = vector(2,2,0)
v2 = vector(1,0,1)

print(v1 == v2) # Liefert False
print(v1 == v1) # Liefert True
Du bist als Entwickler selbstverständlich völlig frei in der Nutzung weiterer magischer Methoden zum Überladen von Operatoren für bestimmte Objekte. Je nach Kontext erhält z. B. der Additionsoperator eine völlig andere Bedeutung und kann von dir eine ganz eigene Logik bekommen.

4. Interaktiver Quellcode