R

ReflexKR

Reflex-Dokumentation

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

Der Etiketten-Flughafen: Pythons Objektmodell
Lektion 8

Zeit 5:50–6:40

Flughafen-Standardverfahren: Dunder-Methoden

Verlustmeldung

LOST-008

Die Kiste, die das Förderband nicht erkennt

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.

8-1. Syntax ruft Dunder-Methoden auf (9 min)

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 iterable

len(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 99

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

SyntaxWas wirklich aufgerufen wird
len(x)type(x).__len__(x)
x[i], x[i] = v__getitem__, __setitem__
a + ba.__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"])

Bei c + d wird in __add__ eine neue Kiste erzeugt, deshalb endet zuerst __init__. print(c) ruft __repr__ auf, weil es kein __str__ gibt. for ruft mangels __iter__ der Reihe nach __getitem__ mit 0 und 1 auf und hört auf, wenn bei 2 ein IndexError kommt (das raise IndexError in der letzten Zeile der Liste).
  • bool(c) fragt stattdessen __len__, weil es kein __bool__ gibt. Ist die Länge nicht 0, ist das Ergebnis wahr.
  • Warum bei 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).

8-2. __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 wenWo er benutzt wird
__repr__für Entwickler. Möglichst in einer Form wie Crate([...]), aus der man das Objekt wieder bauen kannrepr(), interaktive Shell, Debugger, Elemente in Listen und dicts, !r in f-Strings
__str__für Nutzer. Ein gut lesbarer Satzprint(), 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'])
  • Bei print([c]) wurde nicht __str__, sondern __repr__ verwendet. Wird eine Liste in Text umgewandelt, erscheinen die Elemente immer als repr.
  • Fehlt __str__, nehmen str() und print() das __repr__. Umgekehrt gilt das nicht. Wenn du nur einen Griff anbringst, dann zuerst __repr__.
  • Variablenfenster im Debugger, Listen in Logs, Werte in Fehlermeldungen: Das alles ist 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"])

print(c) und f'{c}' rufen __str__ auf, repr(c) und f'{c!r}' rufen __repr__ auf. print([c]) ruft __repr__ auf, weil c ein Element einer Liste ist.

8-3. Das Container-Protokoll (9 min)

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

Ohne __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.
  • Gibt es __contains__, durchläuft in nichts, sondern fragt direkt diese Methode.
  • Ohne __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([])

in geht direkt zu __contains__, for und max gehen direkt zu __iter__. list(c) fragt zusätzlich __len__, um vorab Platz zu reservieren (CPython). bool(e) ruft __len__ auf, weil es kein __bool__ gibt.

8-4. Operatoren: __add__, __radd__, NotImplemented (8 min)

a + b wird in Schritten aufgelöst:

  1. type(a).__add__(a, b) wird aufgerufen.
  2. Ist das Ergebnis NotImplemented, heißt das: „Mit diesem Partner kann ich nichts anfangen.“ Python reicht dann an die Gegenseite weiter, an type(b).__radd__(b, a).
  3. Liefern beide 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.
  • Bei einem unbekannten Partner wirf keine Ausnahme, sondern gib 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"])

Bei 0 + c kennt int die Kiste nicht, also geht es weiter an Crate.__radd__. Bei c + 1 gibt __add__ NotImplemented zurück, und int kennt die Kiste auch nicht, also kommt ein TypeError. c += d hat kein __iadd__ und erzeugt deshalb mit __add__ eine neue Kiste.

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.

8-5. __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 True

with 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 None

Gibt __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__)
# None

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

Fall abgeschlossen (10 min)

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.

Übungen

  1. Ergänze die Crate aus dem Fallabschluss um __eq__ und prüfe, was mit {Crate(["vase"])} passiert. Schreib in einem Satz auf, warum.
  2. Bau eine Klasse, deren __bool__ immer False zurückgibt. Sag voraus, wonach sich if obj: richtet, wenn __len__ 3 ist, und prüfe es dann.
  3. Ändere __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.

Erhoben: deine E-Mail-Adresse · Zweck: Hinweise auf neue Lektionen oder den Start dieses Kurses · Speicherdauer: bis du die Löschung verlangst. Keine andere Nutzung, keine Weitergabe. Antworte einfach auf eine Benachrichtigung, dann löschen wir sie sofort.

Fragen & Antworten

Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.

Wird als reiner Text angezeigt, ohne Links oder Formatierung.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex