Magische Methoden in Python: Dunder-Methoden verstehen und nutzen

Mit Dunder-Methoden wie __repr__, __eq__ oder __add__ machst du eigene Klassen so komfortabel wie Pythons eingebaute Typen. So funktionieren sie – und so nutzt du sie richtig.

Teilen

Hast du dich schon einmal gefragt, warum du in Python einfach len(meine_liste) schreiben kannst oder warum 2 + 3 und "a" + "b" beide mit demselben Pluszeichen funktionieren, obwohl das eine Zahlen addiert und das andere Strings zusammenfügt? Die Antwort steckt in den sogenannten magischen Methoden – auch Dunder-Methoden genannt, weil ihr Name von doppelten Unterstrichen umgeben ist (double underscore). In diesem Beitrag lernst du, was diese Methoden sind und wie du damit eigene Klassen so komfortabel machst wie Pythons eingebaute Typen.

Was sind Dunder-Methoden?

Dunder-Methoden sind spezielle Methoden mit festgelegten Namen wie __init__, __str__ oder __add__. Python ruft sie automatisch auf, wenn du bestimmte Sprachkonstrukte verwendest. Schreibst du len(obj), ruft Python im Hintergrund obj.__len__() auf. Schreibst du a + b, wird a.__add__(b) aufgerufen. Du rufst diese Methoden also fast nie direkt auf – du definierst sie, und Python nutzt sie für dich.

Die wohl bekannteste Dunder-Methode kennst du schon: __init__, der Konstruktor, der beim Erstellen eines Objekts läuft.

class Konto:
    def __init__(self, inhaber, guthaben=0):
        self.inhaber = inhaber
        self.guthaben = guthaben

k = Konto("Anton", 100)
print(k.inhaber)  # Anton

Objekte lesbar machen: __str__ und __repr__

Ohne weitere Angaben zeigt Python bei einem eigenen Objekt nur etwas wie <__main__.Konto object at 0x10f3a2b90> an – wenig hilfreich. Mit __str__ und __repr__ bestimmst du selbst, wie dein Objekt dargestellt wird.

Die Faustregel: __repr__ liefert eine eindeutige, technische Darstellung (idealerweise so, dass man das Objekt daraus wieder erzeugen könnte) und ist für Entwickler gedacht. __str__ liefert eine schöne, lesbare Darstellung für Endnutzer. Fehlt __str__, greift Python automatisch auf __repr__ zurück.

class Konto:
    def __init__(self, inhaber, guthaben=0):
        self.inhaber = inhaber
        self.guthaben = guthaben

    def __repr__(self):
        return f"Konto(inhaber={self.inhaber!r}, guthaben={self.guthaben})"

    def __str__(self):
        return f"Konto von {self.inhaber}: {self.guthaben} Euro"

k = Konto("Anton", 100)
print(k)        # Konto von Anton: 100 Euro   (nutzt __str__)
print(repr(k))  # Konto(inhaber='Anton', guthaben=100)  (nutzt __repr__)

Ein guter Tipp für den Alltag: Definiere zumindest immer __repr__. Es hilft dir enorm beim Debuggen, weil auch Listen von Objekten dann sinnvoll ausgegeben werden.

Objekte vergleichbar machen: __eq__

Standardmäßig gelten zwei Objekte nur dann als gleich, wenn es sich um dasselbe Objekt im Speicher handelt. Zwei getrennt erstellte Konten mit identischem Inhalt sind also nicht gleich – meist nicht das, was du willst. Mit __eq__ legst du fest, wann zwei Objekte inhaltlich gleich sind.

class Punkt:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Punkt):
            return NotImplemented
        return self.x == other.x and self.y == other.y

a = Punkt(1, 2)
b = Punkt(1, 2)
print(a == b)  # True (ohne __eq__ waere es False)

Gibst du NotImplemented zurück, wenn der andere Typ nicht passt, überlässt du Python die Entscheidung – ein sauberes Verhalten. Verwandt dazu sind Methoden wie __lt__ (kleiner als) oder __gt__ (größer als), mit denen du deine Objekte sortierbar machst.

Operatoren überladen: __add__ und Co.

Genau wie das Pluszeichen bei Zahlen und Strings unterschiedlich wirkt, kannst du für eigene Klassen definieren, was +, - oder * bedeuten. Das nennt man Operator-Überladung. Ein klassisches Beispiel ist ein Vektor:

class Vektor:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vektor(self.x + other.x, self.y + other.y)

    def __mul__(self, faktor):
        return Vektor(self.x * faktor, self.y * faktor)

    def __repr__(self):
        return f"Vektor({self.x}, {self.y})"

v1 = Vektor(1, 2)
v2 = Vektor(3, 4)
print(v1 + v2)   # Vektor(4, 6)
print(v1 * 3)    # Vektor(3, 6)

Setze Operator-Überladung mit Bedacht ein: Sie ist nur dann eine gute Idee, wenn die Bedeutung intuitiv klar ist. Ein + für Vektoren oder Geldbeträge ist verständlich – ein +, das im Hintergrund eine Datei speichert, würde nur verwirren.

Objekte wie Container verhalten lassen

Mit __len__, __getitem__ und __contains__ lässt du dein Objekt wie eine Liste oder ein Dictionary wirken. Dann funktionieren len(obj), der Index-Zugriff obj[i] und sogar x in obj und for-Schleifen ganz selbstverständlich.

class Playlist:
    def __init__(self, titel):
        self.titel = titel  # eine Liste von Songs

    def __len__(self):
        return len(self.titel)

    def __getitem__(self, index):
        return self.titel[index]

    def __contains__(self, song):
        return song in self.titel

p = Playlist(["Song A", "Song B", "Song C"])
print(len(p))              # 3
print(p[0])               # Song A
print("Song B" in p)      # True
for song in p:            # dank __getitem__ iterierbar
    print(song)

Das ist die Stärke der Dunder-Methoden: Deine eigenen Klassen fügen sich nahtlos in die Sprache ein, statt eigene, umständliche Methoden wie p.anzahl() oder p.hole(0) zu erzwingen.

Fazit

Dunder-Methoden sind der Schlüssel zu pythonischem Code. Mit __repr__ machst du deine Objekte debug-freundlich, mit __eq__ vergleichbar, mit __add__ und Verwandten rechenbar und mit __len__ oder __getitem__ containerartig. Der große Gewinn: Deine Klassen fühlen sich an wie eingebaute Typen und lassen sich mit den vertrauten Sprachmitteln nutzen. Du musst nicht alle auf einmal lernen – fang mit __init__ und __repr__ an und ergänze die anderen, sobald ein konkreter Anwendungsfall auftaucht. Viel Spaß beim Ausprobieren!