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 8:20–9:10
Verlustmeldung
LOST-011
Gemeldet von
Yoon, nächtliche Anlageninspektion
Was fehlt
Zwei Koffer ohne ein einziges Etikett, die trotzdem nicht entsorgt werden. Seit elf Tagen drehen sie ihre Runden auf Band 13.
Erledigt, wenn
Klären, warum die beiden Koffer nicht entsorgt werden, und sie so wegräumen, dass so etwas nicht wieder entsteht.
Internationaler Flughafen Haneulseom · Gepäckzentrum
Die beiden Koffer auf Band 13, über die seit dem ersten Tag berichtet wird, drehen auch heute ihre Runden. Ein Etikett hat keiner von beiden. Nach Vorschrift müsste ein Koffer ohne Etikett noch am selben Tag zur Entsorgung.
Schichtleiterin Do Miran breitet drei alte Aufzeichnungen auf dem Tisch aus: das Röntgenbild aus Lektion 3, die Notiz aus Lektion 6 und den partner-Eintrag aus Lektion 7. „Heute klären wir, was es mit den beiden auf sich hat. Vorher müssen wir die Entsorgungsregeln genau kennen.“
Jedes Objekt in CPython trägt eine Zahl, die festhält, wie viele Referenzen auf es zeigen. Das ist die Referenzzählung (reference count). Kommt ein Etikett dazu, steigt sie um 1, fällt eins weg, sinkt sie um 1, und in dem Moment, in dem sie 0 erreicht, wird das Objekt auf der Stelle entsorgt.
import sys
a = []
print(sys.getrefcount(a))
# 2
b = a
print(sys.getrefcount(a))
# 3
c = [a, a]
print(sys.getrefcount(a))
# 5
del b, c
print(sys.getrefcount(a))
# 2sys.getrefcount(a) als Argument übergeben wird.c = [a, a] steckt zwei Etiketten in die Liste, also steigt die Zahl um 2. Auch ein Etikett, das in einem Koffer steckt, ist ein Etikett.del b, c fällt der Name b weg, und weil die Liste c entsorgt wird, verschwinden auch die beiden Etiketten darin.Verlass dich nicht auf die Zahl selbst. Seit 3.14 überspringt der Interpreter als Optimierung manche Erhöhungen und Verringerungen. Führt man denselben Code in einer Funktion aus, ist der Anfangswert in CPython 3.14 gleich 1, in 3.13 gleich 2. Worauf es ankommt, ist der Unterschied: „ein Etikett, eins mehr“.
Dass bei 0 sofort entsorgt wird, lässt sich mit __del__ beobachten. Diese Dunder-Methode wird aufgerufen, kurz bevor ein Objekt entsorgt wird (warum man ihr nicht trauen sollte, steht in 11-4).
class Bag:
def __init__(self, label):
self.label = label
def __del__(self):
print("discard", self.label)
a = Bag("A")
b = a
del a
print("after del a")
del b
print("after del b")
# after del a
# discard A
# after del b
def check_in():
tmp = Bag("T")
print("inside")
check_in()
print("back")
# inside
# discard T
# backdel a hängt noch das Etikett b, also wird nichts entsorgt. Erst beim del b, das das letzte Etikett abnimmt, erscheint sofort „discard A“.tmp. Deshalb wird noch vor „back“ entsorgt.Der GC-Beobachtungsstand unten erzeugt in einem Arbeitsprozess echte Objekte und lässt echtes gc laufen. An jedem Koffer steht seine „Refs“-Zahl (Namensschilder + Etiketten in anderen Koffern). Koffer, die man von keinem Namensschild aus über die Kofferinhalte erreichen kann, werden rot gestrichelt gezeichnet. Ein gepunktetes Etikett zeigt mit w() → …, worauf es gerade zeigt.
GC-Beobachtungsstand
Python
Pin sagt
→ Die Zahl von sys.getrefcount hängt von der Version und vom Ort ab (Modul oder Funktion). Achte nicht auf den absoluten Wert, sondern auf den Unterschied von 1, wenn du ein Etikett anhängst oder abnimmst.
Legt man die drei Aufzeichnungen nebeneinander, zeigt sich, was los ist. Das [[...]] auf dem Röntgenbild aus Lektion 3 war ein Koffer, der sich selbst enthält. Laut Lektion 7 zeigten die beiden Koffer über das Attribut partner aufeinander. In Code übertragen sieht das so aus:
Etikettentafel
Schritt 1 / 0
Als Nächstes läuft: 0
Labor wird vorbereitet…
Gerade geändert
Versiegelter Koffer (unveränderlich)
Mit del a, b sind alle Namensschilder abgenommen. In A steckt aber noch ein Etikett, das auf B zeigt, und in B eines, das auf A zeigt. Beide haben den Referenzzähler 1, und der wird nie 0. Das ist ein Referenzzyklus (reference cycle).
Objekte, die von keinem Namen aus erreichbar sind, kann das Programm nie wieder benutzen. Sie sind Müll. Deshalb sind sie auch von der Etikettentafel verschwunden. Mit der Referenzzählung allein lässt sich dieser Müll aber nicht wegräumen.
Deshalb hat CPython einen zweiten Reinigungsdienst: den zyklischen Garbage Collector (Modul gc).
gc.collect() lässt ihn das sofort tun und gibt die Zahl der weggeräumten Objekte zurück.import gc
gc.disable() # automatischen GC kurz abschalten, damit das Beispiel stabil bleibt
class Bag:
def __init__(self, label):
self.label = label
self.partner = None
def __del__(self):
print("discard", self.label)
a = Bag("A")
b = Bag("B")
a.partner = b
b.partner = a
del a, b
print("tags gone")
print(gc.collect() > 0)
gc.enable()
# tags gone
# discard A
# discard B
# Truedel a, b wird gar nichts entsorgt. „discard“ erscheint erst innerhalb von gc.collect(). Die Reihenfolge der beiden Zeilen ist nicht festgelegt.gc.collect() nicht aufrufen. Wenn viele Objekte entstehen, startet der Interpreter ihn von selbst. gc.disable() dient hier nur dem Beispiel.Die Liste aus Lektion 3, die sich selbst enthält, ist derselbe Fall:
import gc
a = []
a.append(a)
print(a, a[0] is a)
# [[...]] True
del a
print(gc.collect())
# z. B.: 23Die zurückgegebene Zahl zählt auch anderen Müll mit, der sich bis dahin angesammelt hat, und hängt deshalb von der Situation und der Version ab. Auf diesem Rechner kam bei derselben Datei mit 3.13.9 eine 1 heraus, mit 3.14.2 eine 23. Der Beobachtungsstand unten räumt beim Start einmal auf und zählt erst danach, deshalb kommt dort jedes Mal derselbe Wert heraus.
GC-Beobachtungsstand
Python
Zyklen werden am Ende also doch weggeräumt. Ist das kein Problem? Zwei Dinge sind anders:
Einen Zyklus beseitigt man auf zwei Arten: Man nimmt nach Gebrauch ein Etikett auf einer Seite selbst ab (wie bei break), oder man macht eine Seite von Anfang an zu einer schwachen Referenz.
Eine schwache Referenz (weak reference) ist ein gepunktetes Etikett, das nicht in den Referenzzähler eingeht. Es zeigt auf einen Koffer, hält ihn aber nicht fest. Wird der Koffer entsorgt, leert sich das gepunktete Etikett von selbst.
import weakref
class Bag:
def __init__(self, label):
self.label = label
a = Bag("A")
w = weakref.ref(a)
print(w() is a)
# True
del a
print(w())
# Nonew(). Ist er schon entsorgt, kommt None heraus, also prüfst du das vor jeder Verwendung.weakref.ref([1, 2]) ergibt TypeError: cannot create weak reference to 'list' object. Mit int, str, tuple und dict ist es genauso, mit Instanzen eigener Klassen funktioniert es.GC-Beobachtungsstand
Python
Am häufigsten werden schwache Referenzen für Caches benutzt. Baut man einen Cache mit einem normalen dict, hält er die abgelegten Objekte für immer fest, und der Speicher schrumpft nie. Ein WeakValueDictionary hält seine Werte mit gepunkteten Etiketten. Werden sie anderswo nicht mehr benutzt, verschwinden die Einträge von selbst.
import weakref
class Bag:
def __init__(self, label):
self.label = label
desk = weakref.WeakValueDictionary()
c = Bag("C")
desk["C"] = c
print("C" in desk, len(desk))
# True 1
del c
print("C" in desk, len(desk))
# False 0Es gibt auch WeakKeyDictionary, das die Schlüssel gepunktet hält, und WeakSet, das die Elemente gepunktet hält. Man nimmt sie, wenn man einem Objekt zusätzliche Informationen anhängen will, ohne seine Lebensdauer zu verlängern.
Soll etwas passieren, wenn ein Koffer entsorgt wird, hinterlegt man es mit weakref.finalize.
import weakref
class Bag:
def __init__(self, label):
self.label = label
d = Bag("D")
f = weakref.finalize(d, print, "discard D")
print(f.alive)
# True
del d
# discard D
print(f.alive)
# Falsefinalize wird höchstens einmal aufgerufen, und lebt der Koffer bis zum Programmende, wird es beim Beenden aufgerufen. Es ist zuverlässiger als das __del__ aus dem nächsten Abschnitt. Aber Vorsicht: Halten die übergebene Funktion oder ihre Argumente den Koffer selbst fest (etwa wenn man d.close übergibt), wird der Koffer nie entsorgt.
__del__ nicht trauen: aufräumen mit with (6 min)__del__ scheint ein guter Ort für Aufräumcode zu sein, ist aber aus drei Gründen unzuverlässig.
__del__ nicht garantiert.class Jam:
def __del__(self):
raise RuntimeError("belt jam")
x = Jam()
del x
print("still running")
# (stderr) z. B.: Exception ignored while calling deallocator <function Jam.__del__ at 0x...>:
# (stderr) ... RuntimeError: belt jam
# still runningRessourcen, die unbedingt geschlossen werden müssen, etwa Dateien, Sockets oder Sperren, schließt du mit with. Sobald der Block verlassen wird, wird __exit__ aufgerufen, selbst wenn eine Ausnahme auftritt (Lektion 8).
class Belt:
def __enter__(self):
print("belt on")
return self
def __exit__(self, *exc):
print("belt off")
return False
with Belt() as belt:
print("moving")
# belt on
# moving
# belt off
try:
with Belt():
raise ValueError("stuck")
except ValueError as e:
print("caught", e)
# belt on
# belt off
# caught stuckAuch die Datei, die open() zurückgibt, benutzt man als with open(path) as f:. So steht der Zeitpunkt des Aufräumens sichtbar im Code.
Pin sagt
→ __del__ verspricht kein „Wann“. Was unbedingt geschlossen werden muss, schließt du mit with. Aufräumarbeiten, die an die Lebensdauer eines Objekts gebunden sind, erledigst du mit weakref.finalize.
Objekte, die überall benutzt werden, wie None, True oder kleine Ganzzahlen, sind unsterbliche Objekte (3.12, PEP 683). Ihr Referenzzähler ist fest, und so viele Etiketten man auch anhängt oder abnimmt, sie werden nie entsorgt.
import sys
print(sys.getrefcount(None))
# 3221225472
x = None
print(sys.getrefcount(None))
# 3221225472
print(sys._is_immortal(None), sys._is_immortal([]))
# True Falsesys._is_immortal gibt es seit 3.14. Der Name beginnt mit einem Unterstrich, es ist also ein Implementierungsdetail von CPython.sys.getsizeof misst nur das Gewicht des Koffers selbst. Den Inhalt zählt es nicht mit.
import sys
print(sys.getsizeof([]), sys.getsizeof([1, 2, 3]))
# 56 88
big = [0] * 1000
print(sys.getsizeof(big), sys.getsizeof([big]))
# 8056 64Unter 64-Bit-CPython besteht eine Liste aus einem leeren Rumpf von 56 Byte plus 8 Byte für jeden Platz, in dem ein Etikett stecken kann. Je nach Erzeugungsart wird großzügig Platz reserviert, deshalb hat [1, 2, 3] nicht 80, sondern 88 Byte. [big] ist ein Koffer mit einem einzigen Platz und hat 64 Byte. Dass das big darin 8056 Byte hat, wird nicht mitgezählt.
Ein Wort zum Free-Threading-Build (3.14t). In einem Build ohne GIL ändern mehrere Threads gleichzeitig Referenzzähler. Deshalb zählt der Thread, der ein Objekt erzeugt hat, seinen Anteil schnell, während andere Threads einen gemeinsamen Anteil separat und atomar zählen (biased reference counting). Die sichtbaren Regeln sind dieselben. Mit 3.14.3t (sys._is_gil_enabled() ergibt False) verhielten sich die Beispiele dieser Lektion genauso, von den Werten des Referenzzählers über das sofortige Entsorgen bis dazu, dass gc.collect() die Zyklen wegräumt. Anders war nur die Zahl, die gc.collect() zurückgab.
Die beiden Koffer waren ein Referenzzyklus: Über ihr partner hielten sie einander fest. Sie hatten kein Etikett, doch ihr Referenzzähler blieb bei je 1. Deshalb wurden sie nicht entsorgt und blieben auf dem Band, bis der zyklische GC lief. Als die Schichtleiterin gc.collect() laufen lässt, ist Band 13 leer.
Damit so etwas nicht wieder entsteht, merkt sich jeder Koffer seinen Partner mit einem gepunkteten Etikett. Verpackt in ein property aus Lektion 10, bleibt der Code beim Benutzen unverändert.
import weakref
class Bag:
def __init__(self, label):
self.label = label
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) # den Partner mit einem gepunkteten Etikett merken
a = Bag("A")
b = Bag("B")
a.partner = b
b.partner = a
print(a.partner.label, b.partner.label)
# B A
watch = weakref.ref(a)
del a
print(watch() is None, b.partner)
# True NoneMit der einen Zeile del a wird A sofort entsorgt, und das partner von B wird zu None. Einen Zyklus, auf den der GC warten müsste, gibt es von vornherein nicht.
Kurz vor Feierabend bringt Yoon noch eine Zeile aus den Aufzeichnungen. Geisterkoffer B lag letzten Monat schon einmal im Fundsachen-Schließfach, und damals konnte ihn niemand herausholen. Die Schichtleiterin legt den Schließfachschlüssel auf den Tisch: „Das sehen wir uns morgen zum Schluss an.“
Abschlussbericht — LOST-011. Die beiden Geisterkoffer waren ein Referenzzyklus, sie hielten einander über partner fest. Mit gc.collect() weggeräumt. Gegen einen Rückfall merkt sich jeder Koffer seinen Partner jetzt mit weakref.ref.
sys.getrefcount(a) nach a = [] und a.append(a) ergibt. Was kommt heraus, wenn du es im Modul ausführst, und warum ist es nicht 2?weakref.WeakValueDictionary(). Welcher Fehler kommt, und was ändert sich, wenn du ihn in eine Instanz einer eigenen Klasse verpackst?__del__ schließt, und bau sie dann so um, dass sie mit __enter__ und __exit__ schließt. Prüfe, ob die Datei auch dann geschlossen wird, wenn im Block eine Ausnahme auftritt.Das Notizbuch der Schichtleiterin
Referenzzähler = Zahl der Etiketten an einem Koffer. In der Zeile, in der er 0 wird, wird sofort entsorgt.
sys.getrefcount zählt auch die als Argument übergebene Referenz. Die Werte hängen von der Version ab, achte nur auf den Unterschied.
Koffer, die einander enthalten (Referenzzyklus), erreichen nie 0. Der zyklische GC räumt sie später weg. gc.collect() gibt die Zahl der weggeräumten Objekte zurück.
Zeitpunkt und Generationen des automatischen GC hängen von der Version ab. Darauf verlässt man sich nicht.
Eine schwache Referenz ist ein gepunktetes Etikett. Für Caches WeakValueDictionary, für Aufgaben beim Entsorgen weakref.finalize.
__del__ traut man nicht. Ressourcen, die geschlossen werden müssen, schließt man mit with.
Unsterbliche Objekte wie None werden nie entsorgt. getsizeof misst nur den Rumpf des Koffers.
← Zurück
Lektion 10 · Die Kontrolle: das Geheimnis des Attributzugriffs
Weiter →
Lektion 12 · Schließfachnummern: Hash, Gleichheit und Gesamtbild
Ü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.