Zum Inhalt springen
Python Fortgeschritten 2 min

Magic Methods (Dunder Methods) in Python

Lerne die wichtigsten Magic Methods in Python kennen: von __init__ ueber Vergleichs- und Arithmetik-Operatoren bis hin zu iterierbaren und aufrufbaren Objekten.

Aktualisiert:

Magic Methods (Dunder Methods) in Python

Magic Methods - auch Dunder Methods genannt (von “double underscore”) - geben deinen Klassen besondere Faehigkeiten. Sie bestimmen, wie sich deine Objekte bei Standard-Operationen verhalten.

Was sind Magic Methods?

Magic Methods sind spezielle Methoden mit doppelten Unterstrichen am Anfang und Ende: __name__. Python ruft sie automatisch auf, wenn du bestimmte Operationen durchfuehrst:

# Was du schreibst:          # Was Python aufruft:
len(objekt)                   # objekt.__len__()
str(objekt)                   # objekt.__str__()
objekt + anderes              # objekt.__add__(anderes)
objekt == anderes             # objekt.__eq__(anderes)
objekt[0]                     # objekt.__getitem__(0)
for x in objekt:              # objekt.__iter__()

Du hast __init__ und __str__ bereits kennengelernt. Jetzt entdecken wir die ganze Welt der Magic Methods!

init und del

__init__ initialisiert ein Objekt, __del__ wird aufgerufen, wenn es zerstoert wird:

class Datenbankverbindung:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.verbunden = True
        print(f"Verbindung zu {host}:{port} hergestellt.")

    def __del__(self):
        if self.verbunden:
            self.verbunden = False
            print(f"Verbindung zu {self.host}:{self.port} geschlossen.")

# Verbindung wird beim Erstellen hergestellt
db = Datenbankverbindung("localhost", 5432)
# Verbindung zu localhost:5432 hergestellt.

# Wenn das Objekt geloescht wird:
del db
# Verbindung zu localhost:5432 geschlossen.

Hinweis: Verlasse dich nicht auf __del__ fuer wichtige Aufraeumarbeiten. Verwende stattdessen Context Manager (with-Statement) oder explizite close()-Methoden.

str und repr

Diese beiden Methoden steuern die Textdarstellung:

class Temperatur:
    def __init__(self, celsius):
        self.celsius = celsius

    def __str__(self):
        """Fuer den Benutzer - huebsch und lesbar."""
        return f"{self.celsius}°C"

    def __repr__(self):
        """Fuer den Entwickler - eindeutig und reproduzierbar."""
        return f"Temperatur({self.celsius})"

t = Temperatur(23.5)
print(t)         # 23.5°C          (nutzt __str__)
print(repr(t))   # Temperatur(23.5)  (nutzt __repr__)
print(f"Es sind {t}")  # Es sind 23.5°C  (nutzt __str__)

# In einer Liste wird __repr__ verwendet:
temps = [Temperatur(20), Temperatur(25), Temperatur(30)]
print(temps)  # [Temperatur(20), Temperatur(25), Temperatur(30)]

Wichtig: Wenn du nur eine implementierst, waehle __repr__. Python nutzt __repr__ als Fallback, wenn __str__ fehlt.

Vergleichs-Methods

Diese Methoden ermoeglichen Vergleiche zwischen Objekten:

MethodeOperatorBedeutung
__eq__==Gleich
__ne__!=Ungleich
__lt__<Kleiner als
__le__<=Kleiner oder gleich
__gt__>Groesser als
__ge__>=Groesser oder gleich
class Schuelernote:
    NOTEN_TEXT = {
        1: "sehr gut", 2: "gut", 3: "befriedigend",
        4: "ausreichend", 5: "mangelhaft", 6: "ungenuegend"
    }

    def __init__(self, wert):
        if wert < 1 or wert > 6:
            raise ValueError("Note muss zwischen 1 und 6 liegen.")
        self.wert = wert

    def __eq__(self, other):
        if isinstance(other, Schuelernote):
            return self.wert == other.wert
        return NotImplemented

    def __lt__(self, other):
        """Kleinere Note = bessere Leistung!"""
        if isinstance(other, Schuelernote):
            return self.wert < other.wert
        return NotImplemented

    def __le__(self, other):
        if isinstance(other, Schuelernote):
            return self.wert <= other.wert
        return NotImplemented

    def __gt__(self, other):
        if isinstance(other, Schuelernote):
            return self.wert > other.wert
        return NotImplemented

    def __ge__(self, other):
        if isinstance(other, Schuelernote):
            return self.wert >= other.wert
        return NotImplemented

    def __str__(self):
        return f"{self.wert} ({self.NOTEN_TEXT[self.wert]})"

    def __repr__(self):
        return f"Schuelernote({self.wert})"

# Vergleiche
mathe = Schuelernote(2)
deutsch = Schuelernote(3)
sport = Schuelernote(1)

print(mathe < deutsch)    # True (2 < 3, also besser)
print(sport == Schuelernote(1))  # True
print(deutsch > mathe)    # True (3 > 2, also schlechter)

# Sortierung funktioniert automatisch!
noten = [deutsch, mathe, sport]
noten.sort()
for note in noten:
    print(note)
# 1 (sehr gut)
# 2 (gut)
# 3 (befriedigend)

Tipp: NotImplemented (ohne raise!) zurueckgeben, wenn der Vergleich nicht moeglich ist. Python versucht dann den umgekehrten Vergleich.

Arithmetische Methods

Diese Methoden ermoeglichen mathematische Operationen:

MethodeOperatorMethodeOperator
__add__+__radd__+ (umgekehrt)
__sub__-__rsub__- (umgekehrt)
__mul__*__rmul__* (umgekehrt)
__truediv__/__floordiv__//
__mod__%__pow__**
__iadd__+=__isub__-=
class Geld:
    def __init__(self, betrag, waehrung="EUR"):
        self.betrag = round(betrag, 2)
        self.waehrung = waehrung

    def __add__(self, other):
        if isinstance(other, Geld):
            if self.waehrung != other.waehrung:
                raise ValueError(f"Kann {self.waehrung} und {other.waehrung} nicht addieren!")
            return Geld(self.betrag + other.betrag, self.waehrung)
        if isinstance(other, (int, float)):
            return Geld(self.betrag + other, self.waehrung)
        return NotImplemented

    def __radd__(self, other):
        """Fuer: 10 + Geld(5) (andere Seite)"""
        return self.__add__(other)

    def __sub__(self, other):
        if isinstance(other, Geld):
            if self.waehrung != other.waehrung:
                raise ValueError(f"Kann {self.waehrung} und {other.waehrung} nicht subtrahieren!")
            return Geld(self.betrag - other.betrag, self.waehrung)
        if isinstance(other, (int, float)):
            return Geld(self.betrag - other, self.waehrung)
        return NotImplemented

    def __mul__(self, faktor):
        if isinstance(faktor, (int, float)):
            return Geld(self.betrag * faktor, self.waehrung)
        return NotImplemented

    def __rmul__(self, faktor):
        return self.__mul__(faktor)

    def __iadd__(self, other):
        """Fuer: geld += anderes"""
        ergebnis = self.__add__(other)
        self.betrag = ergebnis.betrag
        return self

    def __neg__(self):
        """Fuer: -geld"""
        return Geld(-self.betrag, self.waehrung)

    def __abs__(self):
        """Fuer: abs(geld)"""
        return Geld(abs(self.betrag), self.waehrung)

    def __eq__(self, other):
        if isinstance(other, Geld):
            return self.betrag == other.betrag and self.waehrung == other.waehrung
        return NotImplemented

    def __lt__(self, other):
        if isinstance(other, Geld) and self.waehrung == other.waehrung:
            return self.betrag < other.betrag
        return NotImplemented

    def __str__(self):
        return f"{self.betrag:.2f} {self.waehrung}"

    def __repr__(self):
        return f"Geld({self.betrag}, '{self.waehrung}')"

# Verwendung
preis = Geld(19.99)
rabatt = Geld(5.00)

print(preis + rabatt)    # 24.99 EUR
print(preis - rabatt)    # 14.99 EUR
print(preis * 3)         # 59.97 EUR
print(2 * preis)         # 39.98 EUR  (nutzt __rmul__)

preis += Geld(10)
print(preis)             # 29.99 EUR  (nutzt __iadd__)

# sum() funktioniert dank __radd__:
einkauf = [Geld(2.50), Geld(3.99), Geld(12.00)]
gesamt = sum(einkauf, Geld(0))
print(f"Gesamt: {gesamt}")  # Gesamt: 18.49 EUR

len, getitem, setitem

Diese Methoden machen dein Objekt zu einer Art Container:

class Playlist:
    def __init__(self, name):
        self.name = name
        self._songs = []

    def hinzufuegen(self, song):
        self._songs.append(song)

    def __len__(self):
        """len(playlist) funktioniert."""
        return len(self._songs)

    def __getitem__(self, index):
        """playlist[0] funktioniert - auch Slicing!"""
        if isinstance(index, slice):
            neue_playlist = Playlist(f"{self.name} (Auswahl)")
            for song in self._songs[index]:
                neue_playlist.hinzufuegen(song)
            return neue_playlist
        return self._songs[index]

    def __setitem__(self, index, song):
        """playlist[0] = 'Neuer Song' funktioniert."""
        self._songs[index] = song

    def __delitem__(self, index):
        """del playlist[0] funktioniert."""
        del self._songs[index]

    def __str__(self):
        songs_text = "\n".join(f"  {i+1}. {s}" for i, s in enumerate(self._songs))
        return f"Playlist '{self.name}' ({len(self)} Songs):\n{songs_text}"

# Verwendung
rock = Playlist("Rock Klassiker")
rock.hinzufuegen("Stairway to Heaven")
rock.hinzufuegen("Bohemian Rhapsody")
rock.hinzufuegen("Hotel California")
rock.hinzufuegen("Smoke on the Water")

print(len(rock))       # 4
print(rock[0])         # Stairway to Heaven
print(rock[-1])        # Smoke on the Water

rock[1] = "We Will Rock You"  # Ersetzt Bohemian Rhapsody
del rock[3]                     # Loescht Smoke on the Water

# Slicing funktioniert auch:
auswahl = rock[0:2]
print(auswahl)

contains (in-Operator)

class Stundenplan:
    def __init__(self):
        self.faecher = {}

    def fach_hinzufuegen(self, tag, fach):
        if tag not in self.faecher:
            self.faecher[tag] = []
        self.faecher[tag].append(fach)

    def __contains__(self, fach):
        """Ermoeglicht: 'Mathe' in stundenplan"""
        for tag_faecher in self.faecher.values():
            if fach in tag_faecher:
                return True
        return False

    def __str__(self):
        zeilen = []
        for tag, faecher in self.faecher.items():
            zeilen.append(f"{tag}: {', '.join(faecher)}")
        return "\n".join(zeilen)

plan = Stundenplan()
plan.fach_hinzufuegen("Montag", "Mathe")
plan.fach_hinzufuegen("Montag", "Deutsch")
plan.fach_hinzufuegen("Dienstag", "Physik")

print("Mathe" in plan)    # True
print("Kunst" in plan)    # False

if "Physik" in plan:
    print("Du hast Physik im Stundenplan!")

iter und next

Diese Methoden machen dein Objekt iterierbar (verwendbar in for-Schleifen):

class Countdown:
    """Ein iterierbarer Countdown."""

    def __init__(self, start):
        self.start = start

    def __iter__(self):
        """Gibt einen Iterator zurueck."""
        self.aktuell = self.start
        return self

    def __next__(self):
        """Gibt den naechsten Wert zurueck."""
        if self.aktuell < 0:
            raise StopIteration  # Signalisiert das Ende
        wert = self.aktuell
        self.aktuell -= 1
        return wert

# Verwendung in for-Schleife
for zahl in Countdown(5):
    print(zahl, end=" ")
# 5 4 3 2 1 0

# Oder manuell:
cd = Countdown(3)
iterator = iter(cd)
print(next(iterator))  # 3
print(next(iterator))  # 2
print(next(iterator))  # 1
print(next(iterator))  # 0
# print(next(iterator))  # StopIteration!

Hier ein komplexeres Beispiel mit einer eigenen Range-Klasse:

class MeinRange:
    """Eine vereinfachte Version von range()."""

    def __init__(self, start, stop=None, step=1):
        if stop is None:
            self.start = 0
            self.stop = start
        else:
            self.start = start
            self.stop = stop
        self.step = step

    def __iter__(self):
        aktuell = self.start
        while (self.step > 0 and aktuell < self.stop) or \
              (self.step < 0 and aktuell > self.stop):
            yield aktuell  # yield macht dies zu einem Generator
            aktuell += self.step

    def __len__(self):
        return max(0, (self.stop - self.start + self.step - 1) // self.step)

    def __contains__(self, wert):
        if self.step > 0:
            return self.start <= wert < self.stop and (wert - self.start) % self.step == 0
        return self.stop < wert <= self.start and (self.start - wert) % abs(self.step) == 0

# Verwendung
for x in MeinRange(5):
    print(x, end=" ")  # 0 1 2 3 4

print()
for x in MeinRange(2, 10, 3):
    print(x, end=" ")  # 2 5 8

print()
print(5 in MeinRange(10))  # True
print(len(MeinRange(0, 100, 5)))  # 20

call (aufrufbare Objekte)

Mit __call__ kannst du ein Objekt wie eine Funktion aufrufen:

class Multiplizierer:
    """Ein aufrufbares Objekt, das mit einem festen Faktor multipliziert."""

    def __init__(self, faktor):
        self.faktor = faktor

    def __call__(self, wert):
        return wert * self.faktor

verdoppeln = Multiplizierer(2)
verdreifachen = Multiplizierer(3)

print(verdoppeln(5))      # 10
print(verdreifachen(5))   # 15

# Funktioniert auch mit map/filter:
zahlen = [1, 2, 3, 4, 5]
ergebnis = list(map(verdoppeln, zahlen))
print(ergebnis)  # [2, 4, 6, 8, 10]

Ein praktischeres Beispiel - ein Zaehler mit Zustand:

class Zaehler:
    """Ein Zaehler, der sich bei jedem Aufruf erhoeht."""

    def __init__(self, start=0, schritt=1):
        self.wert = start
        self.schritt = schritt
        self.aufrufe = 0

    def __call__(self):
        self.wert += self.schritt
        self.aufrufe += 1
        return self.wert

    def zuruecksetzen(self):
        self.wert = 0
        self.aufrufe = 0

    def __str__(self):
        return f"Zaehler(wert={self.wert}, aufrufe={self.aufrufe})"

z = Zaehler(start=0, schritt=5)
print(z())   # 5
print(z())   # 10
print(z())   # 15
print(z)     # Zaehler(wert=15, aufrufe=3)

@property als Alternative zu Gettern/Settern

@property ist streng genommen kein Magic Method, aber eng damit verwandt. Es erlaubt kontrollierten Zugriff auf Attribute:

class Temperatur:
    def __init__(self, celsius):
        self._celsius = celsius  # "privates" Attribut

    @property
    def celsius(self):
        """Getter - wird bei temperatur.celsius aufgerufen."""
        return self._celsius

    @celsius.setter
    def celsius(self, wert):
        """Setter - wird bei temperatur.celsius = wert aufgerufen."""
        if wert < -273.15:
            raise ValueError("Temperatur kann nicht unter -273.15°C liegen!")
        self._celsius = wert

    @property
    def fahrenheit(self):
        """Berechnetes Property - nur Getter, kein Setter."""
        return self._celsius * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, wert):
        self.celsius = (wert - 32) * 5/9

    @property
    def kelvin(self):
        return self._celsius + 273.15

    @kelvin.setter
    def kelvin(self, wert):
        self.celsius = wert - 273.15

    def __str__(self):
        return f"{self._celsius:.1f}°C / {self.fahrenheit:.1f}°F / {self.kelvin:.1f}K"

# Verwendung - sieht aus wie ein normales Attribut!
t = Temperatur(100)
print(t)              # 100.0°C / 212.0°F / 373.1K

t.celsius = 0
print(t)              # 0.0°C / 32.0°F / 273.1K

t.fahrenheit = 98.6
print(t)              # 37.0°C / 98.6°F / 310.1K

# Validierung funktioniert:
# t.celsius = -300    # ValueError: Temperatur kann nicht unter -273.15°C liegen!

Praxis: Eigene Vector-Klasse

import math

class Vector:
    """Ein 2D-Vektor mit allen wichtigen Operationen."""

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

    # Arithmetik
    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented

    def __sub__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x - other.x, self.y - other.y)
        return NotImplemented

    def __mul__(self, other):
        if isinstance(other, (int, float)):
            return Vector(self.x * other, self.y * other)
        if isinstance(other, Vector):
            return self.x * other.x + self.y * other.y  # Skalarprodukt
        return NotImplemented

    def __rmul__(self, other):
        return self.__mul__(other)

    def __neg__(self):
        return Vector(-self.x, -self.y)

    def __abs__(self):
        """Laenge/Betrag des Vektors."""
        return math.sqrt(self.x**2 + self.y**2)

    # Vergleiche
    def __eq__(self, other):
        if isinstance(other, Vector):
            return self.x == other.x and self.y == other.y
        return NotImplemented

    def __lt__(self, other):
        """Vergleich nach Laenge."""
        if isinstance(other, Vector):
            return abs(self) < abs(other)
        return NotImplemented

    # Container-Methoden
    def __len__(self):
        return 2  # Ein 2D-Vektor hat immer 2 Komponenten

    def __getitem__(self, index):
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        raise IndexError("Vector hat nur 2 Komponenten (0 und 1)")

    def __iter__(self):
        yield self.x
        yield self.y

    # Darstellung
    def __str__(self):
        return f"({self.x}, {self.y})"

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

    # Zusaetzliche Methoden
    def laenge(self):
        return abs(self)

    def normalisiert(self):
        l = abs(self)
        if l == 0:
            return Vector(0, 0)
        return Vector(self.x / l, self.y / l)

    def winkel(self):
        """Winkel in Grad."""
        return math.degrees(math.atan2(self.y, self.x))

# Verwendung
a = Vector(3, 4)
b = Vector(1, 2)

print(a + b)        # (4, 6)
print(a - b)        # (2, 2)
print(a * 2)        # (6, 8)
print(3 * b)        # (3, 6)
print(a * b)        # 11 (Skalarprodukt)
print(abs(a))       # 5.0 (Laenge)
print(-a)           # (-3, -4)

print(a[0], a[1])   # 3 4
x, y = a            # Unpacking dank __iter__
print(f"x={x}, y={y}")

print(a.normalisiert())  # (0.6, 0.8)
print(a.winkel())        # 53.13...

# Sortierung nach Laenge
vektoren = [Vector(5, 0), Vector(1, 1), Vector(3, 4)]
vektoren.sort()
for v in vektoren:
    print(f"{v} -> Laenge {abs(v):.2f}")

Praxis: Eigene Collection

class SortierteCollection:
    """Eine Collection, die ihre Elemente immer sortiert haelt."""

    def __init__(self, elemente=None):
        self._daten = sorted(elemente) if elemente else []

    def hinzufuegen(self, element):
        """Fuegt ein Element an der richtigen Position ein."""
        import bisect
        bisect.insort(self._daten, element)

    def entfernen(self, element):
        self._daten.remove(element)

    # Container Magic Methods
    def __len__(self):
        return len(self._daten)

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

    def __setitem__(self, index, wert):
        self._daten[index] = wert
        self._daten.sort()  # Sortierung beibehalten

    def __delitem__(self, index):
        del self._daten[index]

    def __contains__(self, element):
        """Binaere Suche fuer schnelleres Finden."""
        import bisect
        i = bisect.bisect_left(self._daten, element)
        return i < len(self._daten) and self._daten[i] == element

    def __iter__(self):
        return iter(self._daten)

    def __reversed__(self):
        return reversed(self._daten)

    # Vergleiche
    def __eq__(self, other):
        if isinstance(other, SortierteCollection):
            return self._daten == other._daten
        return NotImplemented

    # Arithmetik (Mengenoperationen)
    def __add__(self, other):
        """Vereinigung zweier Collections."""
        if isinstance(other, SortierteCollection):
            return SortierteCollection(self._daten + other._daten)
        return NotImplemented

    # Aufrufbar
    def __call__(self, filter_fn):
        """Filtert die Collection mit einer Funktion."""
        return SortierteCollection(
            [x for x in self._daten if filter_fn(x)]
        )

    # Darstellung
    def __str__(self):
        return f"SortierteCollection({self._daten})"

    def __repr__(self):
        return f"SortierteCollection({self._daten!r})"

    # Bool
    def __bool__(self):
        """Leere Collection = False."""
        return len(self._daten) > 0

# Verwendung
zahlen = SortierteCollection([5, 2, 8, 1, 9])
print(zahlen)  # SortierteCollection([1, 2, 5, 8, 9])

zahlen.hinzufuegen(3)
zahlen.hinzufuegen(7)
print(zahlen)  # SortierteCollection([1, 2, 3, 5, 7, 8, 9])

print(len(zahlen))     # 7
print(zahlen[0])       # 1 (kleinstes)
print(zahlen[-1])      # 9 (groesstes)
print(5 in zahlen)     # True
print(4 in zahlen)     # False

# Als Funktion aufrufen (filtern)
gerade = zahlen(lambda x: x % 2 == 0)
print(gerade)  # SortierteCollection([2, 8])

# Iteration
for z in zahlen:
    print(z, end=" ")  # 1 2 3 5 7 8 9

print()
# Rueckwaerts
for z in reversed(zahlen):
    print(z, end=" ")  # 9 8 7 5 3 2 1

Uebungen

Uebung 1: Klasse “Bruch”

Erstelle eine Klasse Bruch (fuer mathematische Brueche wie 3/4) mit __add__, __sub__, __mul__, __truediv__, __eq__, __lt__, __str__ und __repr__. Kuerze Brueche automatisch mit dem groessten gemeinsamen Teiler.

Uebung 2: Klasse “Matrix”

Erstelle eine 2x2-Matrix-Klasse mit __add__, __mul__ (Matrix-Multiplikation), __getitem__ (Zugriff auf Zeilen), __eq__, __str__ und einer determinante()-Methode.

Uebung 3: Klasse “Warteschlange”

Erstelle eine Warteschlange (Queue) mit __len__, __iter__, __contains__, __bool__, __str__ und den Methoden einreihen() und naechster() (FIFO-Prinzip). Implementiere auch __call__, um mehrere Elemente auf einmal einzureihen.

Uebung 4: Klasse “Temperaturverlauf”

Erstelle eine Klasse, die Temperaturen ueber den Tag speichert. Implementiere __getitem__ (Temperatur zu einer Stunde), __setitem__, __len__, __iter__, __contains__, __add__ (zwei Verlaeufe zusammenfuegen) und Properties fuer durchschnitt, maximum und minimum.

Pro-Tipp: functools.total_ordering

Du musst nicht alle 6 Vergleichsmethoden einzeln implementieren. Mit @total_ordering aus dem functools-Modul reichen __eq__ und eine der anderen:

from functools import total_ordering

@total_ordering
class Prioritaet:
    def __init__(self, stufe):
        self.stufe = stufe

    def __eq__(self, other):
        return self.stufe == other.stufe

    def __lt__(self, other):
        return self.stufe < other.stufe

# Jetzt funktionieren automatisch auch <=, >, >=, !=
p1 = Prioritaet(1)
p2 = Prioritaet(3)
print(p1 <= p2)   # True - automatisch generiert!
print(p2 >= p1)   # True - automatisch generiert!

Das spart Code und vermeidet Fehler. Verwende es immer, wenn du Vergleiche brauchst!

Zurück zum Python Kurs