PCPP1 Block 1 (Teil 1b): Container, Attributzugriff, Vererbung und Komposition
Track Python · PCPP1 Block 1 (35 %, 15 Fragen laut Quelle) · Ziele 1.2 (zweite Hälfte) und 1.3 · ca. 40 Min.
Was du aus Teil a brauchst
Aus Teil 1a solltest du wissen, dass Python Operatoren auf Spezialmethoden abbildet (a + b ruft a.__add__(b)) und dass NotImplemented ein Rückgabewert ist. Hier machst du dasselbe mit Wahrheitswert, Iteration und Attributzugriff und schließt mit Vererbung gegen Komposition ab.
Worum es geht
Dieser Teil deckt die zweite Hälfte von Ziel 1.2 (Wahrheitswert, Container, Attributzugriff) und Ziel 1.3 (Vererbung, Polymorphismus, Komposition, inheritance, polymorphism, composition) von PCPP1 Block 1 ab (laut Quelle 35 % der Punkte, 15 Fragen). Mit __bool__, __len__, __getitem__ und __getattr__ lässt du eigene Klassen auf if obj:, len(), for, in und obj.name reagieren. Mit Komposition entscheidest du, wann eine Klasse etwas hat, statt etwas zu sein.
Von JS/TS her gedacht
| Thema | JS/TS | Python |
|---|---|---|
| Wahrheitswert | if (obj) ist bei Objekten immer wahr |
__bool__, sonst __len__, sonst wahr |
| Iterierbar | [Symbol.iterator]() |
__iter__, ersatzweise __getitem__ mit Index 0, 1, 2, … |
| Fehlendes Attribut abfangen | Proxy mit get-Trap |
__getattr__ (nur wenn nichts gefunden wird) |
| Jeden Zugriff abfangen | Proxy mit get-Trap |
__getattribute__ (bei jedem Zugriff) |
| Strukturelles Typing | TS prüft zur Compile-Zeit, ob die Form passt | Duck Typing: zur Laufzeit zählt, ob die Methode da ist |
| Wiederverwendung | extends, Mixins, Objekt als Feld |
Vererbung (class A(B)) oder Komposition (Attribut) |
Der größte Denkunterschied: In JS ist ein Objekt in if (obj) immer wahr. In Python entscheidet die Klasse über __bool__ und __len__, und eine leere Box ist falsy. Den Proxy mit get-Trap gibt es in Python als zwei getrennte Haken: __getattr__ (nur Notfall) und __getattribute__ (immer).
Konzept in kleinen Schritten
Schritt 1: Wahrheitswert und Container (Ziel 1.2)
bool(obj): erst__bool__, fehlt es, dann__len__(Länge 0 istFalse), fehlt auch das, ist das Objekt wahr.len(obj)braucht__len__.for x in objundlist(obj): erst__iter__. Fehlt es, aber es gibt__getitem__, ruft Pythonobj[0],obj[1], … bis einIndexErrorkommt.x in obj: erst__contains__, sonst Iteration (wie oben).
Schritt 2: __getattr__ gegen __getattribute__ (Ziel 1.2)
__getattribute__(self, name)läuft bei jedem Attributzugriff.__getattr__(self, name)läuft nur, wenn der normale Weg (Instanz, Klasse, Basisklassen) nichts gefunden hat. Fehlt dann auch dort etwas, muss die MethodeAttributeErrorauslösen, sonst funktionierenhasattrundgetattr(obj, name, default)nicht mehr.- In
__getattribute__(und__setattr__) musst du den eigentlichen Zugriff übersuper()oderobject.__getattribute__machen. Einself.xyzdarin ruft wieder__getattribute__: Endlosrekursion (infinite recursion,RecursionError).
Schritt 3: Vererbung, Komposition, Duck Typing, Polymorphismus (Ziel 1.3)
- “ist ein” (is a) ist Vererbung:
Hund(Tier). “hat ein” (has a) ist Komposition: EinAutohat einenMotorals Attribut. - Komposition vor Vererbung (composition over inheritance): Wenn die Beziehung nicht eindeutig “ist ein” heißt, ist das enthaltene Objekt flexibler. Du erbst sonst die gesamte Schnittstelle der Oberklasse, auch Methoden, die nicht passen.
- Duck Typing: Es zählt, ob das Objekt die nötigen Methoden hat, nicht seine Klasse. Wie in TS bei strukturellem Typing, nur zur Laufzeit.
- Polymorphismus (polymorphism): derselbe Aufruf, je nach Klasse anderes Verhalten.
- Inkonsistente MRO: Wenn sich die Reihenfolge nicht widerspruchsfrei bilden lässt, gibt es schon beim
class-Statement einenTypeError. super()in Mehrfachvererbung ruft den nächsten in der MRO der Instanz auf. Rufen alle Klassen konsequentsuper().__init__()auf (kooperative Vererbung, cooperative inheritance), läuft jede Basisklasse genau einmal (Details in Lektion 07).
Falle: Prüfungs- und Praxisklassiker
bool(obj)bei Containerklassen: Ohne__bool__entscheidet__len__. Eine leere Box ist falsy.__getattr__ist nicht__getattribute__: das eine nur im Notfall, das andere immer. Falsche Wahl ergibt überraschende Rangfolge oder Rekursion.- Endlosrekursion: In
__getattr__auf ein Attribut zugreifen, das es noch nicht gibt (z. B. vor__init__, etwa beim Kopieren), ruft__getattr__erneut auf. - Vererbung nur zum Wiederverwenden: Wer von
listerbt, weilappendbequem ist, erbt auchsortundinsert. Wenn nicht eindeutig “ist ein” gilt, nimm Komposition. - Inkonsistente MRO:
class C(A, B)mitB(A)ist einTypeErrorschon bei der Klassendefinition, nicht erst beim Aufruf.
Übungen
Übung 1: Wahrheitswert und Iteration vorhersagen
class Reihe:
def __getitem__(self, i):
if i >= 4:
raise IndexError
return i * 10
class Stapel:
def __init__(self, *e):
self.e = list(e)
def __len__(self):
return len(self.e)
class Flagge(Stapel):
def __bool__(self):
return TrueTrage das Tupel der sechs Werte ein:
(list(Reihe()), 20 in Reihe(), bool(Stapel()), bool(Flagge()), bool(Stapel(0)), len(Flagge(1, 2)))Ohne __iter__ und __contains__ gibt es einen Ersatzweg über den Index. Bei bool(...) gilt eine feste Reihenfolge der Methoden, und bei Stapel(0) zählt die Länge, nicht der Inhalt.
antwort = ([0, 10, 20, 30], True, False, True, True, 2)
antwortÜbung 2: Attribute aus einem Dict liefern
Die Klasse Einstellungen speichert ihre Werte in self._werte. Schreibe __getattr__ so, dass e.farbe den Wert aus dem Dict liefert. Anforderungen:
- Gibt es ein echtes Attribut (z. B. nach
e.groesse = 99), gewinnt dieses vor dem Dict-Wert. - Fehlt der Name, kommt ein
AttributeError(damithasattr(e, "x")zuFalsewird undgetattr(e, "x", "d")den Default"d"liefert). copy.copy(e)undcopy.deepcopy(e)funktionieren. Beim Kopieren wird das Objekt ohne__init__erzeugt,_werteexistiert dann zunächst nicht.
Zur Falle dahinter (das Modul copy lernst du in Lektion 13a genauer kennen): Liest __getattr__ selbst self._werte, und _werte fehlt, ruft Python wieder __getattr__ auf, für _werte, dann wieder, und so weiter bis zum RecursionError. Die Methode darf also nur auf Wege zugreifen, die __getattr__ nicht erneut auslösen.
__getattr__ läuft nur, wenn der normale Weg scheitert. Was passiert, wenn du darin self._werte liest und _werte gerade nicht existiert? Und wie kommst du an das Instanz-Dict, ohne erneut __getattr__ auszulösen?
class Einstellungen:
def __init__(self, **werte):
self._werte = werte
def __getattr__(self, name):
werte = self.__dict__.get("_werte", {})
if name in werte:
return werte[name]
raise AttributeError(name)
EinstellungenÜbung 3: Vererbung oder Komposition?
Ein Warenkorb soll Artikel enthalten, den Gesamtpreis berechnen und mit for durchlaufbar sein. Er soll nicht alles können, was eine Liste kann (kein sort, kein insert an beliebiger Stelle). Welche Umsetzung ist die beste?
- a)
class Warenkorb(list): Die Basisklasse bringtappend,lenund Iteration schon mit. - b)
class Warenkorb(Artikel): Der Korb erbt vonArtikel, weil beide einen Preis haben. - c) Eine Liste als Attribut halten und
__iter__und__len__selbst anbieten (Komposition). - d) Eine Funktion mit einer globalen Liste, weil Klassen für Container unnötig sind.
Trage den Buchstaben als String ein, z. B. "a".
Prüfe jede Option mit dem Satz “Ein Warenkorb ist ein …” beziehungsweise “Ein Warenkorb hat ein …”. Frage dich außerdem, welche Methoden der Nutzer des Korbs dann plötzlich aufrufen kann, und ob sich der Korb mehrfach sauber erzeugen lässt.
antwort = "c"
antwortMerksatz und Prüfstein
Merksatz: Für bool gilt erst __bool__, dann __len__, ohne __iter__ zählt der Index, __getattr__ ist der Notnagel und __getattribute__ der Türsteher, und wenn es nicht eindeutig “ist ein” heißt, nimm Komposition.
Prüfstein (offene Frage): Eine Klasse Konfig soll fehlende Attribute aus einem Dict liefern und trotzdem kopierbar sein. Warum ist self._werte in __getattr__ gefährlich, und wie umgehst du das? Und wann würdest du statt class Korb(list) lieber eine Liste als Attribut halten?
Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 1: Fortgeschrittene OOP” mit “1.2 Spezialmethoden (Magic Methods)” und “1.3 Vererbung, Polymorphismus, Komposition”. Nicht in der Quelle, aber mit Python 3.13 ausgeführt und geprüft: die Reihenfolge __bool__, __len__, die Iteration über __getitem__, die Rekursion in __getattr__ beim Kopieren und die Fehlermeldung bei inkonsistenter MRO (bitte prüfen, ob sie in der PCPP1 so geprüft werden). Die Aussage “15 Fragen” und “35 %” ist die Angabe der Quelle (bitte prüfen).