PCPP1 Block 1 (Teil 1a): Reflexion, Vergleich, Hash und Rechenoperatoren
Track Python · PCPP1 Block 1 (35 %, 15 Fragen laut Quelle) · Ziele 1.1 und 1.2 (erste Hälfte) · ca. 40 Min.
Worum es geht
Block 1 der PCPP1 (Fortgeschrittene OOP, Advanced OOP) ist laut Quelle der größte Block: 35 % der Punkte, 15 Fragen. Dieser Teil deckt die Reflexion (reflection, Ziel 1.1) und die erste Hälfte der Spezialmethoden (magic methods, auch Dunder-Methoden, Ziel 1.2) ab: Vergleich, Hash und Rechenoperatoren. Damit sorgst du dafür, dass eigene Klassen auf ==, +, * und sum() reagieren wie eingebaute Typen. Das ist die Grundlage für jede saubere Python-Bibliothek und für viele Prüfungsfragen.
Die Lektion 07 hat die Grundbegriffe, super(), MRO, __repr__, __eq__ und NotImplemented schon eingeführt. Hier vertiefen wir: Hash-Regeln, total_ordering, rechte Operanden, sum() und +=.
Teil 1b behandelt Wahrheitswert, Iteration, __getattr__ gegen __getattribute__, Vererbung, Komposition und Polymorphismus. Bewusst weggelassen (kommt später oder ist nur Randthema): __format__, __index__, __bytes__, __setattr__/__delattr__ im Detail, __instancecheck__/__subclasscheck__ (gehören in Metaklassen), __enter__/__exit__ (Kontextmanager, siehe Lektion 09), Dekoratoren (Ziel 1.4) und alles ab 1.5.
Von JS/TS her gedacht
| Thema | JS/TS | Python |
|---|---|---|
| Operator überladen | gar nicht möglich (a + b auf Objekten ergibt Unsinn oder String) |
__add__, __sub__, __mul__, … |
| String-Darstellung | toString() |
__str__ (Anwender), __repr__ (Entwickler) |
| Gleichheit | === prüft Identität, eigene Logik nur per Methode |
== ruft __eq__, is prüft Identität |
| Als Map-Schlüssel nutzbar | nur Primitive oder dieselbe Referenz (Map) |
__hash__ zusammen mit __eq__ |
| Typprüfung | instanceof |
isinstance(obj, K), issubclass(A, B) |
Der größte Denkunterschied: In JS ist + für eigene Objekte nicht definierbar. In Python ist a + b nur Zuckerguss für a.__add__(b), und wenn das nicht klappt, probiert Python b.__radd__(a).
Konzept in kleinen Schritten
Schritt 1: Reflexion mit isinstance und issubclass (Ziel 1.1)
Reflexion (reflection) heißt: Ein Programm befragt Objekte und Klassen über sich selbst. Laut Quelle geht es hier um zwei Funktionen:
isinstance(obj, K): Istobjeine Instanz vonKoder einer Unterklasse?issubclass(A, B): Ist die KlasseAeine Unterklasse vonB? Jede Klasse ist Unterklasse von sich selbst.- Beide akzeptieren als zweites Argument ein Tupel von Klassen: “irgendeine davon”.
Die letzte Zeile ist eine Klassikerfalle: bool ist eine Unterklasse von int. Darum ist True eine Instanz von int, aber type(True) is int ist False.
Wichtig für die Prüfung: Das erste Argument von isinstance ist ein Objekt, das erste Argument von issubclass eine Klasse. Vertauscht gibt es einen TypeError.
Schritt 2: Vergleich, __eq__ und __hash__ (Ziel 1.2)
Wenn du __eq__ selbst schreibst, setzt Python __hash__ auf None. Die Instanzen sind dann nicht mehr hashbar (hashable): Du kannst sie weder in ein set legen noch als dict-Schlüssel nutzen. Der Grund: Zwei Objekte, die gleich sind, müssen denselben Hashwert haben. Python kann das nicht für dich entscheiden.
Regel: Was == gleich nennt, braucht denselben hash().
Weitere Regeln aus der Quelle:
__ne__(!=) wird in Python 3 automatisch aus__eq__abgeleitet (siehe oben:p != Punkt(1, 2)istFalse).__lt__,__le__,__gt__,__ge__werden nicht abgeleitet. Ohne sie gibt<einenTypeError. Abkürzung:functools.total_orderingergänzt die übrigen, wenn du__eq__und eine Ordnungsmethode (z. B.__lt__) lieferst.
Schritt 3: Rechenoperatoren, rechte Seite, NotImplemented (Ziel 1.2)
a + b ruft zuerst a.__add__(b). Gibt das NotImplemented zurück (ein Rückgabewert, kein raise), probiert Python b.__radd__(a). Erst wenn beide NotImplemented liefern, gibt es den TypeError. Das ist der Mechanismus hinter 5 + obj (__radd__) und 5 * obj (__rmul__).
Die eingebaute sum() startet mit dem Startwert 0 und rechnet 0 + erstes + zweites + .... Ohne __radd__, das die 0 akzeptiert, scheitert sum bei eigenen Klassen.
Und +=? Gibt es __iadd__ (in-place), wird es genutzt und darf das Objekt verändern. Ohne __iadd__ fällt Python auf __add__ zurück und bindet den Namen neu.
Falle: Prüfungs- und Praxisklassiker
__eq__ohne__hash__: Instanzen sind unhashable (TypeErrorbeisetunddict-Schlüssel). Und: gleiche Objekte brauchen gleichen Hash.5 == 5.0istTrue, darum isthash(5) == hash(5.0).__radd__fehlt:sum([...])bricht ab, weil0 + objnicht definiert ist. Umgekehrt gilt: Wer in__radd__alles akzeptiert, macht5 + objplötzlich erlaubt.NotImplementedist kein Fehler:return NotImplemented, nichtraise NotImplementedund nichtreturn None.Nonewäre ein gültiges (falsches) Ergebnis.isinstance(True, int)istTrue,type(True) is intistFalse. Undissubclass(h, K)mit einer Instanz ist einTypeError.
Übungen
Übung 1: Reflexion vorhersagen
class Tier: pass
class Hund(Tier): pass
class Welpe(Hund): pass
w = Welpe()Trage das Tupel der sechs Ausdrücke ein:
(isinstance(w, (str, Tier)),
issubclass(Tier, Welpe),
issubclass(Hund, (Welpe, Hund)),
isinstance(Welpe, Tier),
type(w) is Hund,
isinstance(False, (str, int)))Gehe jeden Ausdruck einzeln durch. Frage dich bei jedem: Ist das erste Argument ein Objekt oder eine Klasse? In welche Richtung läuft die Vererbung? Und was ist Welpe selbst, als Objekt betrachtet?
antwort = (True, False, True, False, False, True)
antwortÜbung 2: Hashbar machen
Die Klasse Preis hat ein __eq__, aber keine Hash-Funktion. Ergänze die fehlende Methode so, dass:
Preis-Objekte insetund alsdict-Schlüssel funktionieren,- zwei gleiche Preise im
setnur einmal vorkommen. Beachte:Preis("EUR", 5) == Preis("EUR", 5.0)istTrue, ==und!=unverändert bleiben.
Der Hashwert muss aus genau den Daten entstehen, die auch __eq__ vergleicht, und zwar so, dass 5 und 5.0 zum selben Wert führen. Welche eingebaute Funktion kombiniert mehrere Werte zu einem Hash?
class Preis:
def __init__(self, waehrung, betrag):
self.waehrung, self.betrag = waehrung, betrag
def __eq__(self, other):
if not isinstance(other, Preis):
return NotImplemented
return (self.waehrung, self.betrag) == (other.waehrung, other.betrag)
def __hash__(self):
return hash((self.waehrung, self.betrag))
PreisÜbung 3: Rechnen mit eigener Klasse und sum()
Schreibe die Operatormethoden für Strecke (Länge in Metern, m):
Strecke + Streckeergibt eine neueStreckemit der Summe.sum([Strecke(1), Strecke(2)])funktioniert. Links darf nur die0(Startwert vonsum) stehen:5 + Strecke(1)bleibt einTypeError.Strecke * ganze_Zahlundganze_Zahl * Streckeergeben eine neueStrecke.- Mit fremden Typen (
Strecke(1) + "x",Strecke(1) * "a") kommt einTypeError, keinAttributeErrorund kein stilles Ergebnis.
Für den TypeError musst du nichts selbst auslösen: Wenn deine Methode bei fremden Typen den richtigen Rückgabewert liefert, entscheidet Python. Überlege, welche Zahl sum zuerst addiert und welche Methode dafür gebraucht wird, und welche Eingaben diese Methode nicht akzeptieren darf.
class Strecke:
def __init__(self, m):
self.m = m
def __repr__(self):
return f"Strecke({self.m})"
def __add__(self, other):
if not isinstance(other, Strecke):
return NotImplemented
return Strecke(self.m + other.m)
def __radd__(self, other):
if other == 0:
return self
return NotImplemented
def __mul__(self, k):
if not isinstance(k, int):
return NotImplemented
return Strecke(self.m * k)
__rmul__ = __mul__
StreckeMerksatz und Prüfstein
Merksatz: Gleich heißt gleicher Hash, und NotImplemented ist ein Rückgabewert, der dem anderen Operanden die Chance gibt.
Prüfstein (offene Frage): Eine Klasse definiert __eq__ und __lt__, aber kein __hash__ und kein __le__. Was passiert bei {obj}, bei obj <= obj2 und bei sorted([obj2, obj])? Wie reparierst du jeden der drei Fälle mit möglichst wenig Code?
Weiter geht es mit Teil 1b: Container, Attributzugriff, Vererbung und Komposition.
Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 1: Fortgeschrittene OOP” mit “1.1 Grundbegriffe und Reflexion” und “1.2 Spezialmethoden (Magic Methods)”. Nicht in der Quelle, aber mit Python 3.13 ausgeführt und geprüft: das Verhalten von += mit und ohne __iadd__, sum() mit __radd__ und dem Startwert 0, functools.total_ordering (nur in der Quelle als Hinweis genannt), die Fehlermeldungen der TypeError und die Unterklassenbeziehung von bool und int (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).