W3docs

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-AusdruckAufgerufener Dunder
str(obj)obj.__str__()
len(obj)obj.__len__()
a + ba.__add__(b)
a == ba.__eq__(b)
item in objobj.__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.

MethodeAufgerufen vonZweck
__repr__repr(), interaktive ShellEindeutige, entwicklerorientierte Darstellung
__str__str(), print(), f-StringsBenutzerfreundliche 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.

OperatorMethodeReflektierte 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)  # True

Abkü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.celsius

Arithmetische Operatoren

Arithmetische Dunders ermöglichen es, Objekte mit +, -, *, /, //, % und ** zu verwenden.

AusdruckMethodeHinweise
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.

MethodeAufgerufen vonWas sie ermöglicht
__len__len(obj)Länge des Containers
__getitem__obj[index]Index- und Slice-Zugriff
__setitem__obj[index] = valZuweisung per Index
__delitem__del obj[index]Löschen per Index
__contains__item in objMitgliedschaftstest

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
# cherry

Iterator-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 von iter(obj) und zu Beginn einer for-Schleife aufgerufen; soll das Iterator-Objekt zurückgeben (in der Regel self).
  • __next__ — wird wiederholt aufgerufen, um den nächsten Wert zu liefern; soll StopIteration auslö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
# 0

Fü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 items

Dies 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))  # True

double 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 der with-Block beginnt; sein Rückgabewert wird an die as-Variable gebunden.
  • __exit__(self, exc_type, exc_val, exc_tb) — wird beim Verlassen des Blocks ausgeführt, ob normal oder durch eine Ausnahme. True zurückzugeben unterdrückt die Ausnahme; False (oder None) 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)  # True

Wenn 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

KategorieMethodeAusgelö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 einer User-Klasse wäre verwirrend.

Für fortgeschrittenere OOP-Muster siehe Python Abstract Classes, Python Encapsulation und Python Polymorphism.

Übungsaufgabe

Übung
Welche Dunder-Methode ruft Python auf, wenn ein Objekt in einem boolean-Kontext wie 'if obj:' verwendet wird?
Welche Dunder-Methode ruft Python auf, wenn ein Objekt in einem boolean-Kontext wie 'if obj:' verwendet wird?
Was this page helpful?