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 7

Zeit 5:00–5:50

Der Koffer-Bauplan: Klassen und Instanzen

Verlustmeldung

LOST-007

Ein Aufkleber aufgeklebt, an allen ist er dran

Gemeldet von

Moon, Transportleitung einer Orchestertournee

Was fehlt

Auf einen einzigen Instrumentenkoffer kam ein Aufkleber „Zerbrechlich“. Jetzt klebt er an allen Koffern, die nach demselben Bauplan gebaut wurden.

Erledigt, wenn

Herausfinden, wo die Aufkleberliste wirklich liegt, und jedem Koffer seine eigene Aufkleberliste geben.

Internationaler Flughafen Haneulseom · Gepäckzentrum

In der Schreibtischschublade von Schichtleiterin Do Miran liegt ein Stapel Koffer-Baupläne. Mit einem einzigen Bauplan lassen sich beliebig viele Koffer derselben Form herstellen. „Der Bauplan ist ein Blatt Papier, die Koffer sind einzelne Stücke. Beides sind Dinge.“

Moon hat nur auf den Geigenkoffer „Zerbrechlich“ geklebt, sieht denselben Aufkleber aber auch am Cellokoffer. Die Schichtleiterin schlägt nicht den Koffer auf, sondern den Bauplan. Dort klebte der Aufkleber.

7-1. Auch der Bauplan ist ein Koffer: Klassenobjekte (7 min)

Die class-Anweisung erzeugt ein Klassenobjekt und bindet es an einen Namen, ist also eine Zuweisung (genau wie def in Lektion 1). Auch eine Klasse ist ein Objekt mit einem Typ, und dieser Typ ist type.

class Case:
    size = "M"
    def __init__(self, owner):
        self.owner = owner

print(type(Case), isinstance(Case, type))
# <class 'type'> True
b = Case("cello")
print(type(b).__name__, type(b) is Case)
# Case True
print(vars(b))
# {'owner': 'cello'}
print([k for k in vars(Case) if not k.startswith("__")])
# ['size']
print(b.size, "size" in vars(b))
# M False
Was darin stecktSo siehst du es
Klasse (Bauplan)Namen aus dem Klassenrumpf: Klassenattribute, Methodenvars(Case), Case.__dict__
Instanz (Koffer)Attribute nur dieses Koffers, meist in __init__ per self.x = ...vars(b), b.__dict__
  • Im __dict__ von b steht nur owner. Trotzdem wird b.size als "M" gelesen.
  • Beim Lesen eines Attributs wird zuerst im __dict__ der Instanz nachgesehen und, wenn es dort fehlt, in der Klasse. Fehlt etwas im Koffer, schlägt man den Bauplan auf.
  • Das __dict__ einer Klasse ist ein schreibgeschütztes Fenster namens mappingproxy. Um ein Klassenattribut zu ändern, weist du es direkt der Klasse zu, etwa Case.size = "L".

7-2. Klassenattribute teilen sich alle (10 min)

Das ist der Bauplan aus der Meldung:

class Case:
    stickers = []
    def __init__(self, owner):
        self.owner = owner

a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(b.stickers, Case.stickers)
# ['FRAGILE'] ['FRAGILE']
print(a.stickers is b.stickers, "stickers" in vars(a))
# True False
  • stickers = [] läuft im Klassenrumpf ein einziges Mal. Die Liste hängt nur einmal am Bauplan.
  • a.stickers steht nicht im __dict__ von a, wird also in der Klasse gefunden, und append verändert diese eine Liste (Koffer verändern).
  • Es ist derselbe Fehlertyp wie bei den Standardargumenten in Lektion 4: „Ein einmal erzeugtes veränderliches Objekt wird von vielen geteilt.“

Was jede Instanz für sich haben soll, hängt man in __init__ an self. Vergleich unten die drei Varianten.

Etikettentafel

Schritt 1 / 0

Als Nächstes läuft: 0

Labor wird vorbereitet…

Gerade geändert

Versiegelter Koffer (unveränderlich)

In der ersten Variante steht die Liste nur im Kasten class Case, die beiden Instanzkästen enthalten nur owner. Bei der __init__-Variante hat jede Instanz ihre eigene Liste, bei der letzten Variante nur a.

Weist man einer Instanz wie in der dritten Variante etwas unter demselben Namen zu, bleibt das Klassenattribut unangetastet, und im __dict__ der Instanz entsteht ein neuer Name. Für diese Instanz ist das Klassenattribut dann verdeckt (dieselbe Form wie das Verdecken eingebauter Namen in Lektion 5).

class Case:
    size = "M"

a = Case()
b = Case()
a.size = "L"
print(a.size, b.size, vars(a))
# L M {'size': 'L'}
Case.size = "S"
print(a.size, b.size)
# L S
del a.size
print(a.size)
# S
  • Ändert man das Klassenattribut, sehen das sofort alle Instanzen, die es nicht verdecken (b).
  • Nimmt man mit del a.size den Namen der Instanz ab, ist wieder der Wert der Klasse zu sehen.

Vergiss nicht, dass auch += eine Zuweisung ist. Wendet man += auf ein Klassenattribut mit einer Zahl an, entsteht in der Instanz ein neuer Name.

class Gate:
    count = 0

g = Gate()
g.count += 1
print(g.count, Gate.count, vars(g))
# 1 0 {'count': 1}

g.count += 1 ist g.count = g.count + 1. Rechts wird die 0 aus der Klasse gelesen, links landet die 1 im __dict__ von g. Soll die Zahl für alle Instanzen gemeinsam zählen, musst du wie bei Gate.count += 1 direkt der Klasse zuweisen.

Pin sagt

→ Eine Liste oder ein dict im Klassenrumpf teilen sich alle Instanzen. Leg in die Klasse nur, was wirklich gemeinsam ist, wie Konstanten oder Standardwerte. Was von Koffer zu Koffer verschieden ist, hängst du in __init__ an self.

7-3. Die Lesereihenfolge: erst die Instanz, dann die Klasse (7 min)

Den Weg, den Python beim Lesen von b.x geht, zeigt das Ablaufdiagramm der Kontrolle. In dieser Lektion genügen Schritt 2 (das __dict__ der Instanz) und Schritt 4 (ein normales Attribut der Klasse). Die Schritte 1, 3 und 5 behandeln wir in Lektion 10.

Das b unten hat size sowohl in der Klasse als auch im eigenen __dict__.

Kontrollablauf

1

class Case:

2

size = "M"

3

stickers = []

4

def __init__(self, owner):

5

self.owner = owner

6

def label(self):

7

return "case of " + self.owner

8

9

b = Case("cello")

10

b.size = "L"

b.

1

Daten-Deskriptor in der Klasse (__get__ + __set__)?

2

Im __dict__ der Instanz?

3

Nicht-Daten-Deskriptor in der Klasse (nur __get__, wie eine Funktion)?

4

Ein normales Klassenattribut?

5

Gibt es __getattr__?

6

AttributeError

Ergebnis: b. →

owner und size werden in Schritt 2 im __dict__ der Instanz gefunden, stickers in Schritt 4 in der Klasse. label ist in Schritt 3 eine Funktion, und weight gibt es nirgends, also kommt ein AttributeError.
  • size steht auch in der Klasse, wird aber zuerst in Schritt 2 gefunden und ist deshalb 'L'. Das Case.size in Klammern in der Ergebniszeile bedeutet: „Die Klasse hat ebenfalls einen Namen dieser Art.“
  • label ist eine Funktion in der Klasse. Funktionen sind besondere Objekte, die in Schritt 3 hängen bleiben. Holt man sie heraus, bekommt man keine einfache Funktion, sondern eine bound method. Darum geht es im nächsten Abschnitt.

7-4. Gebundene Methoden: bei jedem Zugriff neu erzeugt (6 min)

Eine Methode ist eine ganz normale Funktion in der Klasse. Holt man sie über eine Instanz heraus, erzeugt Python ein neues Objekt, das Funktion und Instanz zusammenbindet: eine gebundene Methode.

class Bag:
    def __init__(self, owner):
        self.owner = owner
    def label(self):
        return "bag of " + self.owner

bag = Bag("kim")
print(bag.label is bag.label, bag.label == bag.label)
# False True
print(bag.label.__self__ is bag, bag.label.__func__ is Bag.label)
# True True
print(bag.label(), Bag.label(bag))
# bag of kim bag of kim
print(type(bag.label).__name__, type(Bag.label).__name__)
# method function
  • Jedes Mal, wenn du bag.label schreibst, entsteht eine neue gebundene Methode. Deshalb ergibt is False. Weil dieselbe Funktion an dieselbe Instanz gebunden ist, ergibt == True.
  • __self__ ist die gebundene Instanz, __func__ die ursprüngliche Funktion der Klasse.
  • bag.label() ist dasselbe wie Bag.label(bag). self ist nur das erste Argument, das die gebundene Methode für dich ausfüllt.
  • Wie aus einer Funktion beim Herausholen eine Methode wird, lösen wir in Lektion 10 (Deskriptoren) auf.

Etikettentafel

Schritt 1 / 0

Als Nächstes läuft: 0

Labor wird vorbereitet…

Gerade geändert

Versiegelter Koffer (unveränderlich)

m1 und m2 sind zwei verschiedene bound-method-Kästen, aber __self__ zeigt bei beiden auf dieselbe Instanz und __func__ auf dieselbe Funktion in class Bag.

7-5. Namen als Strings behandeln: vars, getattr, setattr, hasattr (5 min)

Manchmal steht der Attributname erst zur Laufzeit als String fest, zum Beispiel als Schlüssel aus einer Konfigurationsdatei oder als Spaltenname, den jemand auf dem Bildschirm ausgewählt hat.

class Bag:
    def __init__(self, owner):
        self.owner = owner

bag = Bag("kim")
setattr(bag, "color", "red")
print(getattr(bag, "color"), hasattr(bag, "weight"), getattr(bag, "weight", 0))
# red False 0
print(vars(bag))
# {'owner': 'kim', 'color': 'red'}
vars(bag)["tag"] = "T1"
print(bag.tag, vars(bag) is bag.__dict__)
# T1 True
bag.weight
# AttributeError: 'Bag' object has no attribute 'weight'
  • setattr(obj, "x", v) entspricht obj.x = v, getattr(obj, "x") entspricht obj.x. Das dritte Argument von getattr ist der Standardwert, falls das Attribut fehlt.
  • hasattr probiert tatsächlich ein getattr und entscheidet danach, ob ein AttributeError kommt.
  • vars() einer Instanz ist keine Momentaufnahme, sondern das echte __dict__. Das ist anders als locals() in einer Funktion aus Lektion 5.
  • An Python-Objekte kann man jederzeit neue Attribute hängen. Das ist bequem, aber auch ein Tippfehler (bag.colour = ...) wird stillschweigend zu einem neuen Attribut.

7-6. Den Bauplan festziehen: __slots__ und weitere neue Funktionen (5 min)

Schreibt man Attributnamen in __slots__, bekommen Instanzen kein __dict__, und nur die aufgeführten Namen sind erlaubt. In CPython spart das außerdem Speicher, weil nicht jede Instanz ein eigenes Dictionary hat.

class Slim:
    __slots__ = ("owner",)
    def __init__(self, owner):
        self.owner = owner

s = Slim("kim")
print(s.owner, hasattr(s, "__dict__"))
# kim False
s.color = "red"
# AttributeError: 'Slim' object has no attribute 'color' and no __dict__ for setting new attributes

Seit 3.13 haben Klassen __static_attributes__. Darin stehen sortiert die Namen, denen in den Methoden der Klasse per self.name = ... ein Wert zugewiesen wird.

class Trunk:
    def __init__(self, owner):
        self.owner = owner
        self.tags = []
    def pack(self):
        self.weight = 10

print(Trunk.__static_attributes__)
# ('owner', 'tags', 'weight')

Zum Schluss die verzögerte Auswertung von Annotationen in 3.14 (PEP 649, 749). Annotationen im Klassenrumpf werden nicht beim Erzeugen der Klasse ausgewertet, sondern erst beim ersten Lesen von __annotations__. Deshalb entsteht die Klasse auch dann, wenn in einer Annotation ein Name steht, der noch gar nicht definiert ist. Bis 3.13 gab es in der Zeile friend: Missing einen NameError.

class Partner:
    friend: Missing

print("created", Partner.__name__)
# created Partner

class Missing:
    pass

print(Partner.__annotations__)
# {'friend': <class '__main__.Missing'>}

Fall abgeschlossen (10 min)

Die Aufkleberliste kommt weg vom Bauplan und an jeden einzelnen Koffer. Gemeinsame Werte (die Standardgröße) bleiben in der Klasse.

class Case:
    size = "M"
    def __init__(self, owner):
        self.owner = owner
        self.stickers = []

a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(a.stickers, b.stickers)
# ['FRAGILE'] []
print(vars(a))
# {'owner': 'violin', 'stickers': ['FRAGILE']}
print(Case.__static_attributes__)
# ('owner', 'stickers')

Jetzt klebt „Zerbrechlich“ nur noch am Geigenkoffer. Als Moon gegangen ist, holt Pin die Aufzeichnungen zu den Geisterkoffern von Band 13 auf den Bildschirm. Beide Koffer wurden nach demselben Bauplan gebaut, und das Attribut partner des einen zeigt auf den anderen, das partner des anderen wiederum auf den ersten. Die Schichtleiterin zieht mit dem Stift einen Kreis um diese Zeile.

Abschlussbericht — LOST-007. Die Aufkleberliste war ein Klassenattribut, also teilten sich alle Koffer dieselbe Liste. Gelöst mit self.stickers = [] in __init__, sodass jeder Koffer eine neue Liste bekommt.

Übungen

  1. Lass Case.stickers = [] in der Klasse stehen und führe a.stickers += ["FRAGILE"] aus. Was wird aus b.stickers und vars(a)? (Tipp: Das += aus Lektion 2 und das += aus 7-2 passieren gleichzeitig.)
  2. Eine Klasse mit __slots__ = ("owner",) bekommt das Klassenattribut size = "M". Kann man in einer Instanz s.size lesen? Und was ist mit s.size = "L"?
  3. Schreib eine Schleife, die mit getattr die drei Attribute ["owner", "color", "weight"] nacheinander holt und ausgibt, wobei fehlende Attribute als "-" erscheinen.

Das Notizbuch der Schichtleiterin

  • Auch Klassen sind Objekte. type(Case) ist type. Auch die class-Anweisung ist eine Zuweisung, die einen Namen bindet.

  • Instanzen haben ihr eigenes __dict__. Gelesen wird in der Reihenfolge Instanz → Klasse.

  • Klassenattribute teilen sich alle Instanzen. Steckt darin ein veränderliches Objekt, sieht jeder eine Änderung an einer Stelle.

  • Eine Zuweisung unter demselben Namen in der Instanz verdeckt das Klassenattribut. Auch += ist eine Zuweisung.

  • bag.m erzeugt bei jedem Zugriff eine neue gebundene Methode (is ist False, == ist True). __self__, __func__.

  • Mit getattr / setattr / hasattr behandelt man Namen als Strings. vars(obj) ist das echte __dict__.

  • __slots__ verhindert neue Attribute. __static_attributes__ (3.13), verzögerte Auswertung von Annotationen (3.14).

← Zurück

Lektion 6 · Handgepäck: Closures

Weiter →

Lektion 8 · Flughafen-Standardverfahren: Dunder-Methoden

Ü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