Python Magic (Dunder) Methods
Python Magic Methods (__init__, __str__, __repr__, Operator-Overloading, Container- und Kontextmanager-Protokolle) mit Praxisbeispielen.
Magic Methods — auch Dunder-Methoden genannt (kurz für double-underscore, also Doppelunterstrich) — sind spezielle Methoden, deren Namen mit zwei Unterstrichen beginnen und enden, wie __init__ oder __len__. Sie bilden Pythons Hook-System: Indem man sie in eigenen Klassen definiert, legt man fest, wie sich ein Objekt mit eingebauten Operatoren und Funktionen wie +, len(), print(), in und with verhalten soll.
Dunder-Methoden werden niemals direkt aufgerufen. Stattdessen ruft Python sie automatisch im Hintergrund auf:
| Python-Ausdruck | Aufgerufener Dunder |
|---|---|
str(obj) | obj.__str__() |
len(obj) | obj.__len__() |
a + b | a.__add__(b) |
a == b | a.__eq__(b) |
item in obj | obj.__contains__(item) |
with obj as x: | obj.__enter__() / obj.__exit__(...) |
Dieses Kapitel behandelt:
- String-Darstellung —
__repr__und__str__ - Vergleichsoperatoren —
__eq__,__lt__und verwandte Methoden - Arithmetische Operatoren —
__add__,__mul__,__rmul__und weitere - Container-Protokoll —
__len__,__getitem__,__contains__ - Iterator-Protokoll —
__iter__und__next__ - Wahrheitswert —
__bool__ - Aufrufbare Objekte —
__call__ - Kontextmanager-Protokoll —
__enter__und__exit__ __hash__— Objekte als Dictionary-Schlüssel nutzbar machen
Stellen Sie vor dem Lesen sicher, dass Sie mit Python-Klassen und -Objekten und Python-Vererbung vertraut sind. Für berechnete Attribute siehe @property.
String-Darstellung: __repr__ und __str__
Diese beiden Methoden steuern, wie ein Objekt in einen String umgewandelt wird.
| Methode | Aufgerufen von | Zweck |
|---|---|---|
__repr__ | repr(), interaktive Shell | Eindeutige, entwicklerorientierte Darstellung |
__str__ | str(), print(), f-Strings | Benutzerfreundliche Ausgabe |
Ist __str__ nicht definiert, greift Python auf __repr__ zurück. Daher ist es empfehlenswert, immer __repr__ zu definieren und __str__ nur dann zu implementieren, wenn man ein anderes, menschenlesbares Format wünscht.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __repr__(self):
return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"
def __str__(self):
return f'"{self.title}" by {self.author} ({self.pages} pages)'
b = Book("Clean Code", "Robert C. Martin", 431)
print(repr(b)) # Book(title='Clean Code', author='Robert C. Martin', pages=431)
print(str(b)) # "Clean Code" by Robert C. Martin (431 pages)
print(b) # "Clean Code" by Robert C. Martin (431 pages)Das Konvertierungsflag !r innerhalb eines f-Strings ruft repr() für diesen Wert auf und setzt Strings in Anführungszeichen. Dadurch lässt sich die Ausgabe von __repr__ direkt als Python-Code einfügen und ausführen.
Tipp: Ein gutes __repr__ erlaubt es, das Objekt aus seiner Ausgabe zu rekonstruieren. Denken Sie dabei an eval(repr(obj)) == obj als gedankliches Modell (auch wenn dies nicht immer buchstäblich zutrifft).
Vergleichsoperatoren
Alle Python-Vergleichsoperatoren werden auf Dunder-Methoden abgebildet. Definieren Sie diese, wenn ==, <, >, <= oder >= Ihre Objekte sinnvoll vergleichen sollen.
| Operator | Methode | Reflektierte Methode |
|---|---|---|
== | __eq__ | __eq__ |
!= | __ne__ | __ne__ |
< | __lt__ | __gt__ |
<= | __le__ | __ge__ |
> | __gt__ | __lt__ |
>= | __ge__ | __le__ |
Reflektiert bedeutet, dass Python die Methode des rechten Operanden versucht, wenn der linke Operand NotImplemented zurückgibt. Wenn beispielsweise a < b die Methode a.__lt__(b) aufruft und diese NotImplemented zurückgibt, versucht Python die reflektierte Methode: b.__gt__(a).
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsius
def __le__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius <= other.celsius
def __repr__(self):
return f"Temperature({self.celsius}°C)"
t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)
print(t1 == t3) # True
print(t1 < t2) # True
print(t2 > t1) # True — Python derives __gt__ from __lt__ via reflection
print(t1 <= t3) # TrueAbkürzung: Wenn Objekte lediglich sortierbar sein sollen, ohne alle sechs Operatoren einzeln zu implementieren, verwenden Sie den Dekorator @functools.total_ordering. Definieren Sie __eq__ und einen der Operatoren __lt__, __le__, __gt__ oder __ge__, und total_ordering ergänzt den Rest automatisch.
from functools import total_ordering
@total_ordering
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius == other.celsius
def __lt__(self, other):
if not isinstance(other, Temperature):
return NotImplemented
return self.celsius < other.celsiusArithmetische Operatoren
Arithmetische Dunders ermöglichen es, Objekte mit +, -, *, /, //, % und ** zu verwenden.
| Ausdruck | Methode | Hinweise |
|---|---|---|
a + b | __add__ | |
a - b | __sub__ | |
a * b | __mul__ | |
b * a | __rmul__ | rechte Version; wird aufgerufen, wenn b.__mul__(a) NotImplemented zurückgibt |
-a | __neg__ | unäre Negation |
abs(a) | __abs__ | |
a += b | __iadd__ | in-place; greift auf __add__ zurück, falls nicht definiert |
Ein klassisches Anwendungsbeispiel ist eine 2D-Vektorklasse:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # supports: 3 * v
return self.__mul__(scalar)
def __neg__(self):
return Vector(-self.x, -self.y)
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v2 - v1) # Vector(2, 2)
print(v1 * 3) # Vector(3, 6)
print(3 * v1) # Vector(3, 6) — uses __rmul__
print(-v1) # Vector(-1, -2)
print(abs(v2)) # 5.0__rmul__ ermöglicht es, dass 3 * v1 funktioniert. Wenn Python 3 * v1 auswertet, ruft es zunächst int.__mul__(3, v1) auf. Da die eingebaute Ganzzahlklasse nicht weiß, wie sie eine Ganzzahl mit einem Vector multiplizieren soll, gibt sie NotImplemented zurück. Daraufhin versucht Python die reflektierte Methode: v1.__rmul__(3), die erfolgreich ist.
Container-Protokoll
Implementieren Sie diese Methoden, damit sich Ihre Klasse wie eine Sequenz oder Kollektion verhält.
| Methode | Aufgerufen von | Was sie ermöglicht |
|---|---|---|
__len__ | len(obj) | Länge des Containers |
__getitem__ | obj[index] | Index- und Slice-Zugriff |
__setitem__ | obj[index] = val | Zuweisung per Index |
__delitem__ | del obj[index] | Löschen per Index |
__contains__ | item in obj | Mitgliedschaftstest |
Das gemeinsame Definieren von __len__ und __getitem__ genügt, um eine Klasse automatisch iterierbar zu machen — Pythons for-Schleife ruft __getitem__ mit aufsteigenden Indizes ab 0 auf, bis ein IndexError auftritt.
class WordBag:
def __init__(self, *words):
self._words = list(words)
def __len__(self):
return len(self._words)
def __contains__(self, item):
return item in self._words
def __getitem__(self, index):
return self._words[index]
def __repr__(self):
return f"WordBag({self._words!r})"
bag = WordBag("apple", "banana", "cherry")
print(len(bag)) # 3
print("banana" in bag) # True
print("grape" in bag) # False
print(bag[0]) # apple
print(bag[-1]) # cherry
# __len__ + __getitem__ makes the object iterable automatically
for word in bag:
print(word)
# apple
# banana
# cherryIterator-Protokoll
Wenn Sie vollständiges Iterator-Verhalten benötigen (direktes Arbeiten mit iter() und next() oder die Verwendbarkeit an Stellen, die einen Iterator statt einer bloßen Iterable erfordern), definieren Sie sowohl __iter__ als auch __next__:
__iter__— wird voniter(obj)und zu Beginn einerfor-Schleife aufgerufen; soll das Iterator-Objekt zurückgeben (in der Regelself).__next__— wird wiederholt aufgerufen, um den nächsten Wert zu liefern; sollStopIterationauslösen, wenn keine Werte mehr vorhanden sind.
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
self.current = self.start
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(3):
print(n)
# 3
# 2
# 1
# 0Für leistungsfähigere Iterationsmuster — insbesondere lazy Sequenzen, die Werte bei Bedarf erzeugen — siehe Python Generators und Python Iterators.
Wahrheitswert: __bool__
Python ruft __bool__ auf, wenn ein Objekt in einem boolean-Kontext verwendet wird (eine if-Anweisung, while-Schleife, not, and, or). Ist __bool__ nicht definiert, aber __len__ vorhanden, verwendet Python len(obj) != 0 als Wahrheitswert. Sind weder noch definiert, gilt das Objekt stets als wahr.
class Stack:
def __init__(self):
self._data = []
def push(self, item):
self._data.append(item)
def pop(self):
return self._data.pop()
def __len__(self):
return len(self._data)
def __bool__(self):
return len(self._data) > 0
def __repr__(self):
return f"Stack({self._data!r})"
s = Stack()
print(bool(s)) # False — empty stack is falsy
s.push(1)
print(bool(s)) # True
print(len(s)) # 1
if s:
print("stack has items") # stack has itemsDies spiegelt das Verhalten eingebauter Kollektionen wider: Eine leere Liste, ein leeres Dictionary oder ein leeres Set ist falsy; ein nicht leeres ist truthy.
Aufrufbare Objekte: __call__
Das Definieren von __call__ ermöglicht es, eine Instanz wie eine Funktion zu verwenden. Dies ist nützlich für Objekte, die zwischen Aufrufen einen Zustand beibehalten — etwas, das eine einfache Funktion ohne Closure oder globale Variable nicht leisten kann.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(callable(double)) # Truedouble und triple sind gewöhnliche Objekte, werden aber mit () wie Funktionen aufgerufen. Die eingebaute Funktion callable() gibt True für jedes Objekt zurück, das __call__ besitzt.
Dieses Muster ist in Machine-Learning-Frameworks (Schichten, Verlustfunktionen) und in Dekoratorfabriken verbreitet. Siehe Python Decorators für einen eng verwandten Anwendungsfall.
Kontextmanager-Protokoll: __enter__ und __exit__
Die with-Anweisung ist Pythons Methode, eine Ressource zuverlässig einzurichten und wieder freizugeben — auch wenn eine Ausnahme auftritt. Jedes Objekt, das __enter__ und __exit__ definiert, kann als Kontextmanager verwendet werden.
__enter__(self)— wird ausgeführt, wenn derwith-Block beginnt; sein Rückgabewert wird an dieas-Variable gebunden.__exit__(self, exc_type, exc_val, exc_tb)— wird beim Verlassen des Blocks ausgeführt, ob normal oder durch eine Ausnahme.Truezurückzugeben unterdrückt die Ausnahme;False(oderNone) lässt sie weitergeben.
class ManagedFile:
def __init__(self, path, mode="r"):
self.path = path
self.mode = mode
self._file = None
def __enter__(self):
self._file = open(self.path, self.mode)
return self._file # the value bound to the "as" variable
def __exit__(self, exc_type, exc_val, exc_tb):
if self._file:
self._file.close()
return False # do not suppress exceptions
with ManagedFile("/etc/hostname") as f:
content = f.read()
# The file is guaranteed to be closed here, even if an exception occurred inside the block.Der Dekorator contextlib.contextmanager aus der Standardbibliothek ermöglicht es, dieselbe Logik als Generatorfunktion zu schreiben — eine leichtgewichtige Alternative für einfache Fälle. Siehe Python with Statement für eine vollständige Behandlung.
Hashing: __hash__
Python verwendet __hash__, um Objekte in Sets und Dictionaries einzufügen. Das Standard-__hash__ basiert auf der Speicheradresse des Objekts (Identität). Wenn Sie __eq__ überschreiben, setzt Python __hash__ automatisch auf None, wodurch Ihre Objekte nicht hashbar werden — Sie müssen __hash__ explizit definieren, wenn Objekte weiterhin in Sets oder als Dictionary-Schlüssel verwendbar sein sollen.
Die Regel lautet: Objekte, die als gleich gelten, müssen denselben Hash haben.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y)) # hash of an immutable tuple
def __repr__(self):
return f"Point({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1 == p2) # True
print(p1 is p2) # False — different objects in memory
print(hash(p1) == hash(p2)) # True
seen = {p1, p2, p3}
print(len(seen)) # 2 — p1 and p2 are equal, so only one copy kept
print(p1 in seen) # TrueWenn Ihre Klasse veränderlich ist (ihre Felder können nach der Erstellung geändert werden), definieren Sie kein __hash__. Veränderliche Objekte sollten nicht hashbar sein, da eine Änderung ihrer Felder auch ihren Hash ändern würde, wodurch jedes Set oder Dictionary, das sie bereits enthält, beschädigt werden würde.
Übersichtstabelle
| Kategorie | Methode | Ausgelöst durch |
|---|---|---|
| Darstellung | __repr__ | repr(obj), interaktive Shell |
| Darstellung | __str__ | str(obj), print(obj), f-Strings |
| Vergleich | __eq__, __ne__ | ==, != |
| Vergleich | __lt__, __le__, __gt__, __ge__ | <, <=, >, >= |
| Arithmetik | __add__, __sub__, __mul__ | +, -, * |
| Arithmetik | __rmul__, __radd__, … | rechte reflektierte Formen |
| Arithmetik | __neg__, __abs__ | unäres -, abs() |
| Container | __len__ | len(obj) |
| Container | __getitem__, __setitem__, __delitem__ | obj[i], obj[i] = v, del obj[i] |
| Container | __contains__ | item in obj |
| Iterator | __iter__ | iter(obj), for-Schleife |
| Iterator | __next__ | next(obj) |
| Wahrheitswert | __bool__ | bool(obj), if obj: |
| Aufrufbar | __call__ | obj(args) |
| Kontextmanager | __enter__, __exit__ | with obj as x: |
| Hashing | __hash__ | hash(obj), Dictionary-Schlüssel, Sets |
Wann Magic Methods einsetzen
- Verwenden Sie sie, wenn Ihre Klasse einen Wertetyp repräsentiert (einen Punkt, einen Vektor, einen Geldbetrag, einen Datumsbereich) — das Überladen von Operatoren und Vergleichen macht die Klasse intuitiv nutzbar.
- Verwenden Sie sie, wenn Ihre Klasse eine Ressource kapselt (eine Datei, eine Datenbankverbindung, einen Netzwerk-Socket) —
__enter__/__exit__stellt sicher, dass die Ressource stets freigegeben wird. - Verwenden Sie sie, wenn Ihre Klasse eine benutzerdefinierte Kollektion ist — die Container- und Iterator-Protokolle ermöglichen die Verwendung mit
for,in,len()und List Comprehensions. - Vermeiden Sie sie bei gewöhnlichen Anwendungsklassen, die keine Wertetypen oder Container sind. Das Überladen von
+bei einerUser-Klasse wäre verwirrend.
Für fortgeschrittenere OOP-Muster siehe Python Abstract Classes, Python Encapsulation und Python Polymorphism.