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 2:30–3:20
Verlustmeldung
LOST-004
Gemeldet von
Jin, Entwicklung bei einem Start-up für Liefer-Apps
Was fehlt
Eine neue Bestellliste. Bei jedem Aufruf der Funktion, die die Bestellliste anlegt, kommen die Bestellungen früherer Kundschaft gleich mit.
Erledigt, wenn
Herausfinden, wo die alten Bestellungen aufbewahrt wurden, und dafür sorgen, dass jeder Aufruf mit einer leeren Liste beginnt.
Internationaler Flughafen Haneulseom · Gepäckzentrum
Mittagspause. Jin aus der Start-up-Entwicklung stürmt mit dem Laptop herein. Der erste Gast hat nur einen Kaffee bestellt, aber auf dem Beleg des zweiten Gastes stehen Kaffee und Bagel zusammen.
Schichtleiterin Do Miran tippt mit dem Finger auf eine Zeile der Funktionsdefinition. „Wenn du einen Koffer übergibst, wandert nicht der Koffer, sondern es kommt ein Etikett dazu. Und diesen leeren Koffer hier hat man genau einmal gekauft, als die Funktion angelegt wurde.“
Beim Aufruf einer Funktion wird das übergebene Objekt nicht kopiert. Der Parametername in der Funktion wird als zusätzliches Etikett an dasselbe Objekt gehängt. Man nennt das Übergabe per Objektreferenz (call by object reference, call by sharing).
mine = ["laptop"]
def show(bag):
print(bag is mine)
show(mine)
# TrueEs ist nur ein Etikett hinzugekommen, also gelten die zwei Handgriffe aus Lektion 1 ganz genauso:
bag.append(...), sieht man das auch über den Namen draußen. Es ist ja derselbe Koffer.bag = ... neu, wandert nur das Etikett innerhalb der Funktion. Der Name draußen hängt weiter am ursprünglichen Koffer.def pack(bag):
bag.append("charger")
def swap(bag):
bag = ["new"]
bag.append("charger")
mine = ["laptop"]
pack(mine)
print(mine)
# ['laptop', 'charger']
swap(mine)
print(mine)
# ['laptop', 'charger']Auf der Etikettentafel unten taucht zum ersten Mal ein Aufrufrahmen (Frame) auf. Beim Aufruf entsteht links ein Rahmen pack, und das Etikett bag darin zeigt auf denselben Koffer wie mine draußen. Endet die Funktion, verschwindet der Rahmen.
Etikettentafel
Schritt 1 / 0
Als Nächstes läuft: 0
Labor wird vorbereitet…
Gerade geändert
Versiegelter Koffer (unveränderlich)
Hier begegnet uns das += aus Lektion 2 wieder. Je nach Typ ist dasselbe += draußen sichtbar oder nicht.
def bump(n):
n += 1 # int: neues Objekt, nur das Etikett in der Funktion wandert
def extend(lst):
lst += [1] # list: __iadd__, derselbe Koffer wird verändert
count, items = 0, []
bump(count)
extend(items)
print(count, items)
# 0 [1]In Python gibt es keine „Argumente, die eine Variable draußen ändern“. Eine Funktion wirkt nur auf zwei Wegen nach außen: Sie verändert ein übergebenes veränderliches Objekt, oder sie gibt einen Wert zurück.
Pin sagt
→ Zerbrich dir nicht den Kopf über „Wertübergabe oder Referenzübergabe“. Python macht immer dasselbe: Es hängt das Parameter-Etikett zusätzlich an das übergebene Objekt. Ob man draußen etwas sieht, hängt nur davon ab, ob die Funktion das Objekt verändert oder bloß das Etikett umhängt.
Auf das Wesentliche gekürzt, sieht die Funktion aus der Meldung so aus:
def f(x=[]):
x.append(1)
return x
print(f())
# [1]
print(f())
# [1, 1]
print(f.__defaults__)
# ([1, 1],)Der Standardwert [] wird nicht beim Aufruf berechnet, sondern einmal, wenn die def-Anweisung läuft. Die so entstandene Liste hängt im Tupel __defaults__ des Funktionsobjekts und bleibt erhalten, solange die Funktion lebt. Bei jedem Aufruf ohne dieses Argument wird der Parameter x an genau diese eine Liste gehängt.
print(f() is f.__defaults__[0])
# TrueVerfolge auf der Etikettentafel unten die zwei Aufrufe. Die Aufrufrahmen entstehen und verschwinden wieder, aber die Liste in defaults bleibt, und die Bestellungen stapeln sich.
Etikettentafel
Schritt 1 / 0
Als Nächstes läuft: 0
Labor wird vorbereitet…
Gerade geändert
Versiegelter Koffer (unveränderlich)
Die Lösung ist Standard: Als Vorgabe steht None, und in der Funktion wird jedes Mal eine neue Liste angelegt.
def add_order(item, orders=None):
if orders is None:
orders = []
orders.append(item)
return orders
print(add_order("coffee"), add_order("bagel"))
# ['coffee'] ['bagel']
print(add_order.__defaults__)
# (None,)Ist None selbst ein sinnvoller Wert und muss von „kein Argument übergeben“ unterschieden werden, legt man ein Objekt an, das sonst niemand benutzt, und verwendet es als Sentinel. Es gibt dieses Objekt nur einmal, also vergleicht man mit is.
_MISSING = object()
def lookup(key, default=_MISSING):
if default is _MISSING:
return f"no default for {key}"
return default
print(lookup("gate"), lookup("gate", None))
# no default for gate None[], {} und set() als Standardwerte. Die Regel „wird einmal bei def berechnet“ gilt für jeden Standardwert.f.__kwdefaults__.Es hilft, Funktionen in zwei Gruppen einzuteilen: solche, die den übergebenen Koffer an Ort und Stelle verändern, und solche, die das Original in Ruhe lassen und einen neuen Koffer zurückgeben.
nums = [3, 1, 2]
print(nums.sort(), nums)
# None [1, 2, 3]
nums = [3, 1, 2]
out = sorted(nums)
print(out, nums, out is nums)
# [1, 2, 3] [3, 1, 2] FalseIn Python gilt eine Konvention: Funktionen und Methoden, die an Ort und Stelle verändern, geben None zurück. Das trifft auf list.sort(), list.append(), list.reverse(), dict.update() und random.shuffle() zu. Diese Abmachung soll nicht verschleiern, dass das Original geändert wurde.
Daraus entsteht dieser typische Fehler:
nums = [3, 1, 2]
nums = nums.sort()
print(nums)
# NoneVerändert an Ort und Stelle (gibt None zurück) | Gibt ein neues Objekt zurück |
|---|---|
lst.sort() | sorted(lst) |
lst.reverse() | lst[::-1], list(reversed(lst)) |
lst.append(x), lst.extend(xs) | lst + [x] |
d.update(other) | d | other |
Entscheide dich auch bei eigenen Funktionen für eine der beiden Arten. Eine Funktion, die verändert und zusätzlich etwas zurückgibt, verleitet den Aufrufer leicht dazu, die Änderung am Original zu übersehen.
*args, **kwargs und die defensive Kopie (7 min)*args sammelt übrige Positionsargumente in einem neuen Tupel, **kwargs übrige Schlüsselwortargumente in einem neuen dict. Was darin steckt, sind aber die Etiketten der übergebenen Objekte.
def g(*args, **kwargs):
return args, kwargs
items = ["a"]
opts = {"tag": items}
a, k = g(items, **opts)
print(type(a), type(k))
# <class 'tuple'> <class 'dict'>
print(a[0] is items, k is opts, k["tag"] is items)
# True False TrueAuch wenn du mit **opts entpackt übergibst, bekommt die Funktion in kwargs ein neues dict. Fügt die Funktion kwargs einen Schlüssel hinzu, bleibt opts deshalb unverändert. Die Liste, die als Wert darin steckt, ist aber dasselbe Objekt.
Etikettentafel
Schritt 1 / 0
Als Nächstes läuft: 0
Labor wird vorbereitet…
Gerade geändert
Versiegelter Koffer (unveränderlich)
Besonders vorsichtig sein muss eine Funktion, die ein übergebenes veränderliches Objekt länger aufbewahrt. Ändert der Aufrufer das Objekt später, ändert sich das Aufbewahrte mit.
def make_flight(code, passengers):
return {"code": code, "passengers": passengers}
names = ["Ari", "Bo"]
f1 = make_flight("HS1", names)
names.append("Cy")
print(f1["passengers"])
# ['Ari', 'Bo', 'Cy']In solchen Fällen legt die Funktion beim Entgegennehmen gleich ihre eigene Version an. Das nennt man defensive Kopie (defensive copy).
def make_flight(code, passengers):
return {"code": code, "passengers": list(passengers)}
names = ["Ari", "Bo"]
f1 = make_flight("HS1", names)
names.append("Cy")
print(f1["passengers"])
# ['Ari', 'Bo']list(passengers) ist eine flache Kopie (Lektion 3). Weil darin nur Strings stecken, reicht das. Eine Funktion, die das Übergebene nur kurz liest, muss dagegen gar nichts kopieren.
Auch return kopiert kein Objekt. Der Koffer, der in der Funktion entstanden ist, wird an den Aufrufer weitergereicht, und dessen Name hängt sein Etikett daran. Selbst wenn der Rahmen der Funktion verschwindet, bleibt der Koffer erhalten, weil draußen ein Etikett an ihm hängt.
def build():
bag = ["boarding pass"]
return bag
b = build()
print(b)
# ['boarding pass']return a, b sieht aus, als würden mehrere Werte zurückgegeben, liefert aber ein einziges Tupel. Entpackt der Aufrufer es, bekommt jedes Element sein eigenes Etikett.
def stats(xs):
return min(xs), max(xs)
res = stats([4, 9, 2])
print(res, type(res))
# (2, 9) <class 'tuple'>
lo, hi = stats([4, 9, 2])
print(lo, hi)
# 2 9Eine Funktion ohne return gibt None zurück. Auch deshalb geben die „verändernden Funktionen“ aus 4-3 None zurück.
Hier die Funktion aus der Meldung und die korrigierte Fassung:
def new_order(item, cart=[]):
cart.append(item)
return cart
print(new_order("coffee"))
# ['coffee']
print(new_order("bagel"))
# ['coffee', 'bagel']
print(new_order.__defaults__)
# (['coffee', 'bagel'],)
def new_order(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart
print(new_order("coffee"), new_order("bagel"))
# ['coffee'] ['bagel']Die alten Bestellungen lagen in der Liste, die im __defaults__ des Funktionsobjekts hing. Der Server definiert die Funktion einmal und ruft sie dann immer wieder auf, also hat diese Liste bis zum Herunterfahren des Servers die Bestellungen aller Kundschaft gesammelt.
Als Jin gegangen ist, ergänzt die Schichtleiterin: „Die Etiketten fallen ab, wenn die Funktion endet. Aber ein Koffer, den das Funktionsobjekt festhält, fällt nicht ab. Auch eine Funktion ist ein Koffer.“
Abschlussbericht — LOST-004. Das Standardargument cart=[] wurde bei def einmal erzeugt und blieb in __defaults__. Alle Aufrufe teilten sich dieselbe Liste. Gelöst mit dem Sentinel None, sodass jeder Aufruf eine neue Liste erzeugt.
def add(x, seen=set()) ruft seen.add(x) auf und gibt len(seen) zurück. Sag die Ergebnisse von add(1), add(2) und add(1) voraus und sieh dir add.__defaults__ an.def clear(lst): lst = [] und def clear(lst): lst.clear() jeweils auf die Liste draußen?sorted sowie Kopieren und dann sort) eine Funktion, die eine Liste entgegennimmt und eine neue, sortierte Liste zurückgibt, ohne das Original anzurühren.Das Notizbuch der Schichtleiterin
Argumente werden per Objektreferenz übergeben. Ein Parameter ist ein weiteres Etikett am selben Objekt.
Verändert die Funktion das Objekt, sieht man es draußen. Bindet sie neu, sieht man nichts.
Standardargumente werden bei def einmal berechnet und bleiben in __defaults__. Statt veränderlicher Standardwerte: None.
Funktionen, die an Ort und Stelle verändern, geben None zurück (sort gegenüber sorted).
*args und **kwargs sind ein neues Tupel und ein neues dict, die Objekte darin bleiben dieselben. Was lange aufbewahrt wird, defensiv kopieren.
Auch return hängt nur ein Etikett um. Mehrere Rückgabewerte sind ein einziges Tupel.
← Zurück
Lektion 3 · Koffer im Koffer: Container und Kopien
Weiter →
Lektion 5 · Zonen und Ausweise: Gültigkeitsbereiche
Ü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.