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 9:10–10:00
Verlustmeldung
LOST-012
Gemeldet von
Han, Fundsachenstelle
Was fehlt
Das Gepäck von Geisterkoffer B. Es wurde letzten Monat im Fundsachen-Schließfach (einem dict) eingetragen, doch als der Besitzer es abholen wollte, hieß es „nicht vorhanden“. Schließfach L7 ist noch immer verschlossen.
Erledigt, wenn
Mit dem Prinzip, nach dem das Schließfach seine Fächer findet, die Ursache klären, L7 öffnen und das Gepäck unter einem unveränderlichen Schlüssel neu einlagern.
Internationaler Flughafen Haneulseom · Gepäckzentrum
Gestern wurden die beiden Geisterkoffer von Band 13 weggeräumt. Übrig ist nur noch eine Zeile im Aufbewahrungsbuch. Letzten Monat wurde B im Fundsachen-Schließfach eingetragen, doch als der Besitzer es abholen wollte, antwortete das Buch „nicht vorhanden“. B kam zurück aufs Band, und der Rest ist die Geschichte bis gestern.
Han schlägt das Buch von damals noch einmal auf. Gibt man die Liste aus, steht B eindeutig darin, aber fragt man mit in, kommt False heraus. Schichtleiterin Do Miran nimmt die letzte Meldung entgegen. „Die Schließfachnummer bestimmt der Koffer. Ändert sich der Koffer nach dem Einlagern, ändert sich auch die Nummer.“
== bestätigen (8 min)dict und set sind Schließfachanlagen mit vielen Fächern. Beim Einlagern und beim Suchen eines Schlüssels laufen jedes Mal dieselben Schritte ab:
hash(key) wird der Hash (die Schließfachnummer) des Schlüssels bestimmt.== denselben Schlüssel bestätigt. Wenn nicht, geht es nach einer festen Regel zum nächsten Fach.Deshalb braucht es ein Versprechen: Gilt a == b, muss auch hash(a) == hash(b) gelten. Würde derselbe Schlüssel in verschiedenen Fächern mit der Suche beginnen, würden sie sich nie begegnen.
print(1 == 1.0 == True, hash(1) == hash(1.0) == hash(True))
# True True
print({1: "int", 1.0: "float", True: "bool"})
# {1: 'bool'}1, 1.0 und True sind untereinander ==, haben also auch denselben Hash und sind im dict derselbe Schlüssel. Als Schlüssel bleibt die zuerst eingetragene 1, nur der Wert wird durch den letzten ersetzt.
Umgekehrt muss es nicht gelten. Eine Kollision, also gleicher Hash bei verschiedenem Wert, ist normal. Das == in Schritt 4 sortiert sie aus.
print(hash(42), hash(-1), hash(-2))
# 42 -2 -2
print(hash(-1) == hash(-2), -1 == -2)
# True False
lockers = {-1: "A", -2: "B"}
print(lockers[-1], lockers[-2])
# A BIn CPython ist der Hash einer kleinen Ganzzahl die Zahl selbst. Die -1 bedeutet intern in C aber „Fehler“, deshalb ergibt hash(-1) die -2. Das ist eine Kollision, und trotzdem hält das dict die beiden problemlos auseinander.
Der Hash eines Strings ändert sich bei jedem Programmstart. Das soll Angriffe verhindern, die Hashwerte vorausberechnen. Braucht man stets denselben Wert (etwa um Tests reproduzierbar zu machen), legt man die Umgebungsvariable PYTHONHASHSEED fest.
$ python -c "print(hash('T1'))"
# z. B.: 4646783657884298203
$ python -c "print(hash('T1'))"
# z. B.: -1769311965495050168
$ PYTHONHASHSEED=0 python -c "print(hash('T1'))"
# bei jedem Start derselbe WertDeshalb darf man Hashwerte weder in Dateien speichern noch mit anderen Prozessen austauschen. Ein Hash ist eine Nummer, die nur innerhalb eines einzigen Programmlaufs gilt.
Das Schließfach unten ist auf 8 Fächer vereinfacht. Es beginnt im Fach „Hash modulo 8“ und geht, wenn es belegt ist, direkt zum nächsten Fach. Lagere nacheinander 7 und 15 sowie -1 und -2 ein, um Kollisionen zu erzeugen.
Schließfachraum
Eine vereinfachte Hashtabelle mit 8 Fächern. CPythons dict wählt die Fächer in einer komplizierteren Reihenfolge, aber das Prinzip ist gleich: Fach per Hash finden, mit == bestätigen.
Das Schließfach legt den Schlüssel in das Fach mit der Nummer, die beim Einlagern berechnet wurde, und berechnet diese Nummer danach nie wieder. Was passiert, wenn sich der Wert des Schlüssels nach dem Einlagern ändert und damit sein Hash? Wir probieren es mit einem absichtlich falsch gebauten Schlüssel aus.
class Tag:
def __init__(self, n):
self.n = n
def __eq__(self, other):
return isinstance(other, Tag) and self.n == other.n
def __hash__(self):
return hash(self.n) # Fehler: Hash aus einem veränderlichen Wert gebildet
def __repr__(self):
return f"Tag({self.n})"
key = Tag(3)
lockers = {key: "bag B"}
print(Tag(3) in lockers)
# True
key.n = 13
print(lockers)
# {Tag(13): 'bag B'}
print(Tag(13) in lockers, Tag(3) in lockers, key in lockers)
# False False FalseIn der Ausgabe steht Tag(13), doch egal wie man fragt, es kommt False heraus.
Tag(13): Die Suche beginnt im Fach für Hash 13. Der Koffer liegt im Fach für Hash 3, die beiden begegnen sich nicht.Tag(3): Man kommt zwar im Fach für Hash 3 an. Der Schlüssel dort ist jetzt aber Tag(13), also ergibt == False.key selbst: Sein Hash ist jetzt 13, also geht es denselben Weg wie im ersten Fall.# Fortsetzung des Beispiels oben
key.n = 3
print(Tag(3) in lockers)
# TrueSetzt man den Wert auf 3 zurück, wird er wieder gefunden. Der Koffer lag die ganze Zeit in dem Fach, in das er eingelagert wurde. Geändert hat sich nur die Nummer, mit der man das Fach sucht.
Schließfachraum
Eine vereinfachte Hashtabelle mit 8 Fächern. CPythons dict wählt die Fächer in einer komplizierteren Reihenfolge, aber das Prinzip ist gleich: Fach per Hash finden, mit == bestätigen.
Deshalb nimmt Python eingebaute Typen, deren Wert sich ändern kann, gar nicht erst als Schlüssel an. list, dict und set haben keinen Hash.
lockers = {}
lockers[[1, 2]] = "bag"
# TypeError: cannot use 'list' as a dict key (unhashable type: 'list')print(hash((1, 2)) == hash((1, 2)))
# True
hash((1, [2]))
# TypeError: unhashable type: 'list'Ein Tupel kann Schlüssel sein, aber steckt eine Liste darin, lässt sich kein Hash bilden. Wie in Lektion 2 gesehen, bedeutet die Unveränderlichkeit eines Tupels nur, dass seine Referenzen feststehen. Ändert sich die Liste darin, müsste sich auch der Hash ändern. Als Schlüssel nimmt man Werte, die bis ganz innen unveränderlich sind, etwa (1, (2,)) oder ein frozenset.
Pin sagt
→ Schlüssel in dicts und Elemente in sets müssen „nach dem Einlagern unverändert“ bleiben. Musst du etwas ändern, nimm es heraus (pop) und lagere es unter dem neuen Schlüssel wieder ein.
Eine Klasse, die nichts davon definiert, vergleicht auf Gleichheit per is und bildet auch den Hash aus der Identität. Deshalb kann man ihre Instanzen als Schlüssel benutzen. Definiert man aber nur __eq__, setzt Python __hash__ auf None (wie in Lektion 8 angekündigt). Hat sich der Maßstab für Gleichheit geändert und benutzt man trotzdem den alten Hash, wäre das Versprechen gebrochen.
class Label:
def __init__(self, code):
self.code = code
def __eq__(self, other):
return isinstance(other, Label) and self.code == other.code
print(Label("T1") == Label("T1"), Label.__hash__)
# True None
{Label("T1")}
# TypeError: cannot use 'Label' as a set element (unhashable type: 'Label')Braucht man einen Hash, gibt es zwei Wege.
1. @dataclass(frozen=True): erzeugt __eq__ und __hash__ aus den Feldern und verhindert, dass Felder geändert werden.
from dataclasses import dataclass
@dataclass(frozen=True)
class Tag:
n: int
key = Tag(3)
lockers = {key: "bag B"}
print(lockers[Tag(3)])
# bag B
key.n = 13
# dataclasses.FrozenInstanceError: cannot assign to field 'n'Ein @dataclass ohne frozen erzeugt nur __eq__, also ist __hash__ dann None. Bei einer Klasse, die als Schlüssel dienen soll, gehört frozen=True dazu.
2. __hash__ selbst schreiben: Man packt genau die Felder, die __eq__ vergleicht, in ein Tupel und bildet daraus den Hash.
class Route:
def __init__(self, origin, dest):
self.origin = origin
self.dest = dest
def __eq__(self, other):
if not isinstance(other, Route):
return NotImplemented
return (self.origin, self.dest) == (other.origin, other.dest)
def __hash__(self):
return hash((self.origin, self.dest))
seen = {Route("ICN", "NRT")}
print(Route("ICN", "NRT") in seen, len({Route("ICN", "NRT"), Route("ICN", "NRT")}))
# True 1Wer ihn selbst schreibt, hält sich an drei Regeln:
__eq__ vergleicht.NotImplemented statt False zurückgeben (Lektion 8).Als letzte Aufgabe legt die Schichtleiterin das alte Verwaltungsprogramm des Zentrums vor. Alle Fehler aus den elf Meldungen stecken in einer einzigen Datei. Sehen wir uns zuerst das Ergebnis der Ausführung an.
import gc
import weakref
class Bag:
stickers = [] # veränderliche Liste als Klassenattribut
def __init__(self, code, items=[]): # Liste als Standardargument
self.code = code
self.items = items
self.partner = None
def __eq__(self, other):
return isinstance(other, Bag) and self.code == other.code
def __hash__(self):
return hash(self.code) # Hash aus einem veränderlichen Wert
a = Bag("A1")
b = Bag("B1")
a.items.append("camera")
a.stickers.append("fragile")
print(b.items, b.stickers)
# ['camera'] ['fragile']
shelves = [[]] * 3 # dreimal dieselbe Liste
shelves[0].append("A1")
print(shelves)
# [['A1'], ['A1'], ['A1']]
calls = [lambda: f"gate {g}" for g in (1, 2, 3)]
print([call() for call in calls])
# ['gate 3', 'gate 3', 'gate 3']
lockers = {b: "L7"}
b.code = "B2" # Schlüssel nach dem Einlagern geändert
print(Bag("B1") in lockers, Bag("B2") in lockers)
# False False
a.partner, b.partner = b, a # zwei Koffer, die einander enthalten
watch = weakref.ref(a)
del a, b, lockers
print(watch() is None)
# False
gc.collect()
print(watch() is None)
# TrueWir gehen Zeile für Zeile die Ursachen durch und machen daraus eine Prüfliste.
| Symptom | Ursache | Lösung | Lektion |
|---|---|---|---|
| Die Kamera, die in A gelegt wurde, ist auch in B | Das Standardargument items=[] wird nur einmal bei der Definition erzeugt | mit items=None entgegennehmen und darin eine neue Liste anlegen | 4 |
| Der Aufkleber klebt an allen Koffern | Alle Instanzen teilen sich das Klassenattribut stickers = [] | in __init__ als Instanzattribut | 7 |
| Alle drei Regalfächer ändern sich zusammen | [[]] * 3 sind drei Referenzen auf dieselbe Liste | [[] for _ in range(3)] | 3 |
| Jede Benachrichtigung sagt gate 3 | Das Lambda hält nicht den Wert, sondern die Variable g (späte Bindung) | lambda g=g: ... | 6 |
| B wird im Schließfach nicht gefunden | Der Wert des Schlüssels wurde nach dem Einlagern geändert, sein Hash ist ein anderer | unveränderliche Werte als Schlüssel, beim Ändern umlagern | 12 |
| Koffer bleiben, obwohl alle Namensschilder ab sind | Referenzzyklus über die beiden partner | eine Seite mit weakref.ref | 11 |
Hier das korrigierte Programm. Alle Ausgaben sind jetzt so, wie sie gemeint waren.
import weakref
class Bag:
def __init__(self, code, items=None):
self.code = code
self.items = [] if items is None else list(items)
self.stickers = []
self._partner = None
@property
def partner(self):
return self._partner() if self._partner is not None else None
@partner.setter
def partner(self, other):
self._partner = weakref.ref(other)
a = Bag("A1")
b = Bag("B1")
a.items.append("camera")
a.stickers.append("fragile")
print(b.items, b.stickers)
# [] []
shelves = [[] for _ in range(3)]
shelves[0].append("A1")
print(shelves)
# [['A1'], [], []]
calls = [lambda g=g: f"gate {g}" for g in (1, 2, 3)]
print([call() for call in calls])
# ['gate 1', 'gate 2', 'gate 3']
lockers = {b.code: "L7"} # der Schlüssel ist ein unveränderlicher String
lockers["B2"] = lockers.pop(b.code) # beim Ändern des Codes wird der Schlüssel umgelagert
b.code = "B2"
print("B1" in lockers, "B2" in lockers, lockers[b.code])
# False True L7
a.partner, b.partner = b, a
watch = weakref.ref(a)
del a, b, lockers
print(watch() is None)
# TrueDass das Übergebene mit items=list(items) kopiert wird, ist ebenfalls die defensive Kopie aus Lektion 4. __eq__ und __hash__ sind aus Bag verschwunden, also werden die Koffer selbst über ihre Identität verglichen, und die Rolle des Schlüssels übernimmt der Code als String.
Die ersten beiden Fehler (Standardargument, Klassenattribut) vergleichen wir auf der Etikettentafel. Bei bug laufen die items-Pfeile beider Instanzen in einer Liste zusammen, und stickers hängt am Kasten des Bauplans (class).
Etikettentafel
Schritt 1 / 0
Als Nächstes läuft: 0
Labor wird vorbereitet…
Gerade geändert
Versiegelter Koffer (unveränderlich)
Ein Rückblick auf die zwölf Lektionen, je eine Zeile:
| Lektion | Titel | In einer Zeile |
|---|---|---|
| 1 | Das Etikett ist nicht der Koffer: Namen und Objekte | = hängt ein Namensschild an ein Objekt. is heißt dasselbe Objekt, == derselbe Wert. |
| 2 | Versiegelte Koffer: veränderlich und unveränderlich | Eine „Änderung“ an einem unveränderlichen Objekt ist ein neues Objekt plus Neubindung. += hat je nach Typ zwei Gesichter. |
| 3 | Koffer im Koffer: Container und Kopien | Container enthalten Referenzen. Eine flache Kopie geht eine Ebene tief, deepcopy bis ganz nach innen. |
| 4 | Wenn ein Koffer übergeben wird: Funktionen und Argumente | Eine Funktion hängt ein weiteres Etikett an dasselbe Objekt. Standardargumente entstehen nur einmal. |
| 5 | Zonen und Ausweise: Gültigkeitsbereiche | Namen werden in der Reihenfolge L → E → G → B gesucht, und wo zugewiesen wird, bestimmt den Bereich. |
| 6 | Handgepäck: Closures | Innere Funktionen tragen keine Werte mit sich, sondern Variablen (Zellen). |
| 7 | Der Koffer-Bauplan: Klassen und Instanzen | Klassenattribute teilen sich alle Instanzen. Instanzattribute gehören in __init__. |
| 8 | Flughafen-Standardverfahren: Dunder-Methoden | Syntax ruft Dunder-Methoden auf. len(x) ist type(x).__len__(x). |
| 9 | Der Bauplan hinter dem Bauplan: Vererbung und MRO | Attribute werden in der Reihenfolge von __mro__ gesucht, super() ist die nächste Klasse in der MRO. |
| 10 | Die Kontrolle: das Geheimnis des Attributzugriffs | property und Deskriptoren fangen den Attributzugriff ab. Auch Funktionen sind Deskriptoren. |
| 11 | Herrenlose Koffer: Speicher und GC | Bei Referenzzähler 0 wird entsorgt, Zyklen räumt der GC weg. Eine schwache Referenz ist ein gepunktetes Etikett. |
| 12 | Schließfachnummern: Hash, Gleichheit und Gesamtbild | Gilt a == b, dann hash(a) == hash(b). Schlüssel dürfen sich nach dem Einlagern nicht ändern. |
Geisterkoffer B war ein Koffer, der seinen Hash aus der Nummer no bildete. Nach dem Einlagern wurde bei einer Umsortierung die Nummer überschrieben, und damit änderte sich der Weg zum Fach. Geht man ohne Hash Fach für Fach durch, ist B noch da.
from dataclasses import dataclass
class Bag:
def __init__(self, no):
self.no = no
def __eq__(self, other):
return isinstance(other, Bag) and self.no == other.no
def __hash__(self):
return hash(self.no)
ghost = Bag(3)
lockers = {ghost: "L7"}
ghost.no = 13 # Nummer bei der Umsortierung überschrieben
print(ghost in lockers)
# False
print([v for k, v in lockers.items() if k is ghost]) # ohne Hash Fach für Fach durchgehen: da ist er
# ['L7']
@dataclass(frozen=True)
class BagNo:
no: int
lockers = {BagNo(13): "L7"} # unter einem unveränderlichen Schlüssel neu einlagern
print(lockers[BagNo(13)], BagNo(13) in lockers)
# L7 Trueitems() benutzt keinen Hash und findet B deshalb. Dafür dauert es so lange, wie es Fächer gibt.BagNo ist frozen=True, seine Nummer lässt sich nach dem Einlagern nicht mehr ändern. Ändert sich die Nummer, erzeugt man ein neues BagNo und lagert um.Schließfach L7 geht auf. Bs Gepäck, einen Monat lang eingeschlossen, kommt zum Vorschein, und Han verständigt den Besitzer. Schichtleiterin Do Miran legt das Bündel Etiketten zurück in die Schublade. „Weißt du noch, was ich am ersten Tag gesagt habe? Ein Koffer, viele Etiketten. Jetzt siehst du, wo die Etiketten hängen, wer einen Koffer festhält und in welchem Fach ein Koffer liegt, oder?“ Pin zeigt auf Band 13. Heute dreht dort nichts seine Runden.
Abschlussbericht — LOST-012. B war ein Schlüssel, dessen Hash sich nach dem Einlagern geändert hatte, deshalb wurde er im Schließfach nicht gefunden. L7 per items() gefunden und unter einem Schlüssel mit @dataclass(frozen=True) neu eingelagert. Der Fall der Geisterkoffer von Band 13 ist damit vollständig abgeschlossen.
{(1, 2): "A", (2, 1): "B"}? Vergleich für beide Tupel == und hash, sag das Ergebnis voraus und prüfe es dann.Tag aus 12-2 mit @dataclass(frozen=True) neu und prüfe, wo key.n = 13 abgefangen wird.Das Notizbuch der Schichtleiterin
dict und set finden das Fach per Hash und bestätigen mit ==. Gilt a == b, dann hash(a) == hash(b).
Hash-Kollisionen sind normal (hash(-1) == hash(-2)). String-Hashes ändern sich bei jedem Start (PYTHONHASHSEED).
Ändert sich ein Schlüssel nach dem Einlagern, findet man ihn weder mit dem neuen noch mit dem alten Wert. Schlüssel unveränderlich halten.
list, dict und set können keine Schlüssel sein. Ein Tupel mit einer Liste darin auch nicht.
Wer nur __eq__ definiert, bekommt __hash__ = None. @dataclass(frozen=True) nehmen oder __hash__ aus denselben Feldern schreiben.
Prüfliste: geteilte veränderliche Objekte, Standardargumente, späte Bindung, Klassenattribute, Referenzzyklen, geänderte Schlüssel.
Ein Koffer, viele Etiketten. Wenn du feststeckst, zeichne Namensschilder und Koffer.
Ü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.