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.
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:
| Methode | Operator | Bedeutung |
|---|---|---|
__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:
| Methode | Operator | Methode | Operator |
|---|---|---|---|
__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!