Der Etiketten-Flughafen: Pythons Objektmodell
·
Kursüberblick
1
Das Etikett ist nicht der Koffer: Namen und Objekte
2
Versiegelte Koffer: veränderlich und unveränderlich
3
Koffer im Koffer: Container und Kopien
4
Wenn ein Koffer übergeben wird: Funktionen und Argumente
5
Zonen und Ausweise: Gültigkeitsbereiche
6
Handgepäck: Closures
7
Der Koffer-Bauplan: Klassen und Instanzen
8
Flughafen-Standardverfahren: Dunder-Methoden
9
Der Bauplan hinter dem Bauplan: Vererbung und MRO
10
Die Kontrolle: das Geheimnis des Attributzugriffs
11
Herrenlose Koffer: Speicher und GC
12
Schließfachnummern: Hash, Gleichheit und Gesamtbild
Zeit 5:50–6:40
Verlustmeldung
LOST-008
Gemeldet von
Nam, Transport für eine Ausstellung
Was fehlt
Die Klasse Crate für die Kisten mit Ausstellungsstücken. Legt man eine Kiste aufs Förderband, scheitern len(), for und +, und print() gibt nur eine rätselhafte Adresse aus.
Erledigt, wenn
Crate mit den Flughafen-Standardverfahren (Dunder-Methoden) ausstatten, damit eingebaute Funktionen und Operatoren die Kiste erkennen.
Internationaler Flughafen Haneulseom · Gepäckzentrum
Nam verwaltet die Kisten mit Ausstellungsstücken über eine selbst geschriebene Klasse. Die Kisten sind völlig in Ordnung, aber sobald sie aufs Förderband kommen, bleibt alles stehen. Schichtleiterin Do Miran dreht eine Kiste einmal herum. „Das Förderband achtet nicht auf die Form der Kiste. Es greift nach den Griffen. Und diese Kiste hat keinen einzigen Griff.“
Pythons Syntax und eingebaute Funktionen greifen nach Methoden mit festgelegten Namen. Weil vor und nach dem Namen je zwei Unterstriche stehen, heißen sie „Dunder-Methoden“ (double underscore). Heute bringen wir diese Griffe einen nach dem anderen an.
So sieht die Kiste aus der Meldung aus. Wir lassen dieselbe Kiste vier Dinge tun:
class Crate:
def __init__(self, items):
self.items = list(items)
c = Crate(["vase", "lamp"])
for expr in ["len(c)", "c[0]", "c + c", "iter(c)"]:
try:
eval(expr)
except TypeError as e:
print(f"{expr:7} → {e}")
# len(c) → object of type 'Crate' has no len()
# c[0] → 'Crate' object is not subscriptable
# c + c → unsupported operand type(s) for +: 'Crate' and 'Crate'
# iter(c) → 'Crate' object is not iterablelen(c) ist keine Zauberei. Python ruft type(c).__len__(c) auf. Diese Methode gibt es nicht, also kommt der Fehler „hat kein len“. Bringt man __len__ an, klappt es sofort.
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
c = Crate(["vase", "lamp"])
print(len(c), type(c).__len__(c))
# 2 2
c.__len__ = lambda: 99
print(len(c), c.__len__())
# 2 99Die letzten beiden Zeilen sind wichtig. Selbst wenn man __len__ an die Instanz hängt, bleibt len(c) bei 2. Die Dunder-Methoden, die die Syntax aufruft, werden nicht in der Instanz, sondern im Typ (der Klasse) gesucht. Das Förderband folgt nur den Verfahren, die im Bauplan stehen.
| Syntax | Was wirklich aufgerufen wird |
|---|---|
len(x) | type(x).__len__(x) |
x[i], x[i] = v | __getitem__, __setitem__ |
a + b | a.__add__(b), sonst b.__radd__(a) |
a == b | __eq__ |
v in x | __contains__ (ohne: durchlaufen und vergleichen) |
for v in x | __iter__ (ohne: __getitem__) |
bool(x), if x: | __bool__ (ohne: __len__) |
print(x), str(x) | __str__ (ohne: __repr__) |
x() | __call__ |
with x: | __enter__, __exit__ |
Eine Klasse mit gut angebrachten Griffen nennt man oft „pythonisch“. Man muss sich keine eigenen Methoden wie size() oder merge() merken, sondern arbeitet mit dem bekannten len und +.
Das Stellwerk unten gibt der Crate mit Griffen jeweils eine Zeile Syntax und zeigt der Reihe nach, welche Methoden Python dabei tatsächlich aufgerufen hat.
Dunder-Stellwerk
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __len__(self):
7
return len(self.items)
8
def __getitem__(self, i):
9
return self.items[i]
10
def __add__(self, other):
11
return Crate(self.items + other.items)
12
13
c = Crate(["vase", "lamp"])
14
d = Crate(["cup"])
bool(c) fragt stattdessen __len__, weil es kein __bool__ gibt. Ist die Länge nicht 0, ist das Ergebnis wahr.for x in c die Methode __getitem__ aufgerufen wird, sehen wir in 8-3.Pin sagt
→ Dunder-Methoden bringst du nur an. Aufrufen tust du sie über die Syntax, also len(c) statt c.__len__(). Die Syntax sucht im Typ und kümmert sich auch um die Ausweichwege (an __radd__ weiterreichen, __len__ statt __bool__ fragen).
__repr__ und __str__ (7 min)Wenn die Meldenden die Kiste zum Debuggen ausgaben, kam nur so etwas heraus:
class Crate:
def __init__(self, items):
self.items = list(items)
print(Crate(["vase"]))
# z. B.: <__main__.Crate object at 0x000001F4C2A8B2C0>Das von object geerbte Standard-__repr__ liefert nur Typname und Adresse. Was in der Kiste steckt, erfährt man nicht. Für die Umwandlung in Text gibt es zwei Griffe.
| Text für wen | Wo er benutzt wird | |
|---|---|---|
__repr__ | für Entwickler. Möglichst in einer Form wie Crate([...]), aus der man das Objekt wieder bauen kann | repr(), interaktive Shell, Debugger, Elemente in Listen und dicts, !r in f-Strings |
__str__ | für Nutzer. Ein gut lesbarer Satz | print(), str(), {x} in f-Strings |
class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __str__(self):
return f"crate of {len(self.items)}"
c = Crate(["vase", "lamp"])
print(c)
# crate of 2
print(repr(c))
# Crate(['vase', 'lamp'])
print([c])
# [Crate(['vase', 'lamp'])]
print(f"{c} / {c!r}")
# crate of 2 / Crate(['vase', 'lamp'])print([c]) wurde nicht __str__, sondern __repr__ verwendet. Wird eine Liste in Text umgewandelt, erscheinen die Elemente immer als repr.__str__, nehmen str() und print() das __repr__. Umgekehrt gilt das nicht. Wenn du nur einen Griff anbringst, dann zuerst __repr__.repr. Was du bei der Fehlersuche siehst, ist dein __repr__.Dunder-Stellwerk
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __str__(self):
7
return f"crate of {len(self.items)}"
8
9
c = Crate(["vase", "lamp"])
Damit sich eine Kiste wie ein Container verhält, genügen fünf Griffe: __len__, __getitem__, __iter__, __contains__ und __bool__. In 8-1 lief for aber sogar ohne __iter__.
class OldCrate:
def __init__(self, items):
self.items = list(items)
def __getitem__(self, i):
print("getitem", i)
return self.items[i]
for x in OldCrate(["vase", "lamp"]):
pass
# getitem 0
# getitem 1
# getitem 2
print("lamp" in OldCrate(["vase", "lamp"]))
# getitem 0
# getitem 1
# TrueOhne __iter__ baut iter() einen Iterator, der __getitem__ der Reihe nach mit 0, 1, 2 … aufruft. Kommt ein IndexError, versteht er das als „Ende“ und hört still auf. Deshalb erscheint noch getitem 2, und trotzdem endet alles ohne Fehler. Ohne __contains__ holt in die Elemente genauso einzeln heraus und vergleicht mit ==. Bei einem Treffer hört es auf.
Dieser Weg existiert nur aus Gründen der Abwärtskompatibilität. Neue Klassen bekommen ordentliche Griffe.
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
def __iter__(self):
yield from self.items
def __contains__(self, x):
return x in self.items
c, e = Crate(["vase", "lamp"]), Crate([])
print(list(c), "lamp" in c, bool(c), bool(e))
# ['vase', 'lamp'] True True False
if not e:
print("empty crate")
# empty crate__iter__ muss einen Iterator zurückgeben. return iter(self.items) geht auch. Mit yield wie oben wird die Funktion zum Generator und erzeugt den Iterator von selbst. Generatoren vertiefen wir im Kurs über Generatoren und Dekoratoren.__contains__, durchläuft in nichts, sondern fragt direkt diese Methode.__bool__ entscheidet sich der Wahrheitswert daran, ob __len__ 0 ist. Fehlen beide, ist das Objekt immer wahr. Deshalb funktioniert if not e: bei einer leeren Crate ganz natürlich, sobald sie __len__ hat.Dunder-Stellwerk
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __len__(self):
5
return len(self.items)
6
def __iter__(self):
7
yield from self.items
8
def __contains__(self, x):
9
return x in self.items
10
11
c = Crate(["vase", "lamp"])
12
e = Crate([])
__add__, __radd__, NotImplemented (8 min)a + b wird in Schritten aufgelöst:
type(a).__add__(a, b) wird aufgerufen.NotImplemented, heißt das: „Mit diesem Partner kann ich nichts anfangen.“ Python reicht dann an die Gegenseite weiter, an type(b).__radd__(b, a).NotImplemented, gibt es einen TypeError.class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
if other == 0:
return self
return NotImplemented
c, d = Crate(["vase"]), Crate(["cup"])
print(c + d)
# Crate(['vase', 'cup'])
print(sum([c, d]))
# Crate(['vase', 'cup'])
c + 1
# TypeError: unsupported operand type(s) for +: 'Crate' and 'int'sum() beginnt bei 0 und rechnet zuerst 0 + c. int.__add__(0, c) kennt keine Kisten und gibt NotImplemented zurück, also wird Crate.__radd__(c, 0) aufgerufen. Weil __radd__ die 0 akzeptiert, funktioniert sum.NotImplemented zurück. Nur so bekommt die Gegenseite eine Chance. Das ist etwas anderes als NotImplementedError (eine Ausnahme).Dunder-Stellwerk
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __add__(self, other):
7
if not isinstance(other, Crate):
8
return NotImplemented
9
return Crate(self.items + other.items)
10
def __radd__(self, other):
11
if other == 0:
12
return self
13
return NotImplemented
14
15
c = Crate(["vase"])
16
d = Crate(["cup"])
Hier löst sich das Rätsel der zwei Gesichter von += aus Lektion 2. a += b sucht zuerst __iadd__ (Addition an Ort und Stelle). Gibt es das nicht, wird daraus a = a + b, und das Etikett wandert zu einem neuen Objekt. list hat __iadd__, tuple und str haben es nicht.
c = Crate(["vase"])
alias = c
c += Crate(["cup"])
print(c is alias, alias)
# False Crate(['vase'])
class Crate2(Crate):
def __iadd__(self, other):
self.items += other.items
return self
c = Crate2(["vase"])
alias = c
c += Crate2(["cup"])
print(c is alias, alias)
# True Crate(['vase', 'cup'])Gibt es __iadd__, verändert es den Inhalt des Koffers und gibt sich selbst zurück. Python hängt das Etikett c erneut an diesen Rückgabewert. Es ist derselbe Koffer, also bleibt das Etikett, wo es war. (class Crate2(Crate) ist ein Bauplan, der von Crate erbt. Das ist das Thema von Lektion 9.)
Pin sagt
→ Vergisst du in __iadd__ das return self, ist c nach c += d plötzlich None. Python hängt das Etikett nämlich an das, was __iadd__ zurückgibt.
__call__, with und @dataclass (7 min)Mit __call__ kann man ein Objekt wie eine Funktion aufrufen. Das ist praktisch, wenn man eine Funktion braucht, die Einstellungen mit sich trägt (dasselbe wie die Closures aus Lektion 6, nur mit einer Klasse).
class Scale:
def __init__(self, limit):
self.limit = limit
def __call__(self, kg):
return kg <= self.limit
check = Scale(23)
print(check(20), check(30), callable(check))
# True False Truewith ruft __enter__ und __exit__ auf. Selbst wenn im Block eine Ausnahme auftritt, wird __exit__ garantiert aufgerufen. Deshalb nimmt man es zum Aufräumen von Ressourcen, und in Lektion 11 begegnet es uns wieder.
class Belt:
def __enter__(self):
print("belt on")
return self
def __exit__(self, exc_type, exc, tb):
print("belt off", exc_type)
return False
with Belt() as b:
print("moving")
# belt on
# moving
# belt off NoneGibt __exit__ True zurück, wird die Ausnahme verschluckt. Meistens ist False (oder gar keine Rückgabe) richtig.
Griffe, die man immer ähnlich schreibt, wie __init__, __repr__ und __eq__, erzeugt @dataclass für dich.
from dataclasses import dataclass
@dataclass
class Tag:
code: str
kg: int = 0
t = Tag("ICN", 12)
print(t, t == Tag("ICN", 12))
# Tag(code='ICN', kg=12) True
print(Tag.__hash__)
# NoneSieh dir die letzte Zeile an. Definiert man __eq__, setzt Python __hash__ auf None. Bei selbst geschriebenen Klassen ist das genauso.
class Crate:
def __init__(self, items):
self.items = list(items)
def __eq__(self, other):
return isinstance(other, Crate) and self.items == other.items
print(Crate(["vase"]) == Crate(["vase"]), Crate.__hash__)
# True None
{Crate(["vase"])}
# TypeError: cannot use 'Crate' as a set element (unhashable type: 'Crate')Hätte man festgelegt „gleicher Wert heißt gleich“, der Hash richtete sich aber weiter nach der Identität (der Adresse), dann landeten zwei gleiche Kisten in verschiedenen Fächern eines set. Damit dieses Versprechen nicht gebrochen wird, ist der Hash ganz abgeschaltet. Das Versprechen zwischen Hash und Gleichheit sehen wir uns in Lektion 12 genauer an.
Hier die Crate mit allen Griffen:
class Crate:
def __init__(self, items=()):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __len__(self):
return len(self.items)
def __getitem__(self, i):
return self.items[i]
def __iter__(self):
return iter(self.items)
def __contains__(self, x):
return x in self.items
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
return self if other == 0 else NotImplemented
a, b = Crate(["vase", "lamp"]), Crate(["frame"])
print(len(a), a[0], "lamp" in a, [x.upper() for x in a])
# 2 vase True ['VASE', 'LAMP']
print(a + b, sum([a, b]))
# Crate(['vase', 'lamp', 'frame']) Crate(['vase', 'lamp', 'frame'])
print(a[-1], a[:1])
# lamp ['vase']Bei a[:1] wurde ein Slice-Objekt an __getitem__ übergeben, und zurück kam einfach der Slice der Liste. Soll eine Kiste zurückkommen, unterscheidet man mit isinstance(i, slice).
Nams Kisten gleiten jetzt leise über das Förderband. Die Schichtleiterin sagt: „Die Form ist dieselbe geblieben. Wir haben nur Griffe angebracht.“
Abschlussbericht — LOST-008. Crate hatte keine Griffe (Dunder-Methoden), deshalb erkannte die Syntax die Kiste nicht. Gelöst mit repr, len, getitem, iter, contains, add und radd.
Crate aus dem Fallabschluss um __eq__ und prüfe, was mit {Crate(["vase"])} passiert. Schreib in einem Satz auf, warum.__bool__ immer False zurückgibt. Sag voraus, wonach sich if obj: richtet, wenn __len__ 3 ist, und prüfe es dann.__getitem__ so, dass es bei isinstance(i, slice) eine Crate zurückgibt und a[:1] damit Crate(['vase']) wird.Das Notizbuch der Schichtleiterin
Syntax ruft Dunder-Methoden auf. len(x) ist type(x).__len__(x). Gesucht wird im Typ, nicht in der Instanz.
Wenn nur einer, dann __repr__. print nimmt __repr__, wenn es kein __str__ gibt, und Elemente in Listen sind immer repr.
for nimmt __iter__, ohne es __getitem__ ab 0 (Schluss beim IndexError). bool nimmt __bool__, ohne es __len__.
Bei unbekanntem Partner NotImplemented zurückgeben. Dann bekommt das __radd__ der Gegenseite eine Chance.
a += b arbeitet an Ort und Stelle, wenn es __iadd__ gibt (return self), sonst a = a + b.
Wer __eq__ definiert, bekommt __hash__ = None. Bei @dataclass genauso.
← Zurück
Lektion 7 · Der Koffer-Bauplan: Klassen und Instanzen
Weiter →
Lektion 9 · Der Bauplan hinter dem Bauplan: Vererbung und MRO
Über neue Lektionen informiert werden
Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.
R
ReflexKR
Python und Reflex zum Anfassen. Alle Beispiele wurden mit Reflex 0.10.0 ausgeführt und geprüft.
ReflexKR ist kostenlos und ohne Werbebanner. Wenn es dir geholfen hat, kannst du es mit einem Kaffee unterstützen. ☕ Spendier uns einen Kaffee
Hinweis zu Affiliate-Links© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Fragen & Antworten
Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.