PCPP1 Block 1 (Teil 2b): Abstrakte Klassen, Properties, Builtins beerben

Track Python · PCPP1 Block 1 (Ziele 1.6 bis 1.8) · ca. 50 Min.

Was du aus Teil a brauchst

Aus Teil 2a brauchst du vor allem, dass @name über einer Methode eine Funktion durch das Ergebnis eines Aufrufs ersetzt. @property, @name.setter und @abstractmethod sind genau solche Dekoratoren, nur eben eingebaute.

Worum es geht

Dieser Teil deckt die Ziele 1.6 bis 1.8 von PCPP1 Block 1 ab (laut Quelle 35 % der Punkte, 15 Fragen, Stand der Quelle: 1. Oktober 2026, bitte vor der Buchung prüfen): abstrakte Klassen (abstract classes), Kapselung mit Properties (encapsulation) und das Beerben eingebauter Typen wie list und dict. Als JS/TS-Entwickler kennst du abstract, get/set und extends Array. Neu sind die Laufzeitfehler und Fallen, an denen Prüfungsfragen ansetzen.

Von JS/TS her gedacht

Thema JS/TS Python
abstract class abstract class A { abstract f(): void } (nur Compiler) ABC plus @abstractmethod, Laufzeitfehler beim Instanziieren
Getter/Setter get x() und set x(v) @property und @x.setter
Von Array oder Map erben class L extends Array class L(list), aber interne Methoden umgehen deine Überschreibung

Dieselbe Idee, ein Getter mit Validierung, in beiden Sprachen:

class Konto {
  private _stand = 0;
  get stand() { return this._stand; }
  set stand(v: number) {
    if (v < 0) throw new RangeError("negativ");
    this._stand = v;
  }
}
const k = new Konto();
k.stand = 5;   // ruft den Setter, Zugriff sieht aus wie ein Feld

In Python sieht der Zugriff genauso aus. Der Unterschied liegt in der Schreibweise: Der Name steht dreimal da (@property, @stand.setter, gleicher Methodenname), und ein Tippfehler im Namen erzeugt still eine zweite Eigenschaft. Dazu Schritt 2.

Konzept in kleinen Schritten

Schritt 1: Abstrakte Klassen (Ziel 1.6)

  • Eine abstrakte Klasse erbt von ABC (Modul abc) und markiert Methoden mit @abstractmethod. Sie lässt sich nicht instanziieren.
  • Eine Unterklasse muss alle abstrakten Methoden überschreiben. Sonst ist sie selbst abstrakt, und Unterklasse() löst einen TypeError aus.
  • Eine abstrakte Methode darf einen Rumpf haben, den Unterklassen über super() aufrufen können.
  • Abstrakte Eigenschaft: @property über @abstractmethod.

Der Fehler kommt beim Instanziieren, nicht beim Definieren der Unterklasse. Anders als bei TypeScript prüft das nicht der Compiler, sondern Python zur Laufzeit.

Drei Details, die in Prüfungen als Falle dienen:

  1. Teilweise überschrieben heißt weiter abstrakt.
  2. @abstractmethod ohne ABC (genauer: ohne die Metaklasse ABCMeta) schützt vor nichts (mit Python ausgeführt). Die Quelle nennt unter den typischen Fallen die Gegenrichtung: ABC ohne @abstractmethod schützt auch vor nichts.
  3. Eine Unterklasse darf eine abstrakte Methode auch durch ein Klassenattribut gleichen Namens ersetzen (a = 1). Der Name zählt dann als implementiert (mit Python ausgeführt, nicht in der Quelle, bitte prüfen).

Schritt 2: Kapselung mit Properties (Ziel 1.7)

Ein Getter liest, ein Setter schreibt (mit Prüfung), ein Deleter löscht. Mit @property, @name.setter, @name.deleter. Ohne Setter ist die Eigenschaft schreibgeschützt.

Zwei Stellen, an denen es schiefgeht:

  • self._stand = stand im __init__ umgeht den Setter. Konto(-1) wäre dann erlaubt.
  • Namen müssen gleich sein. Heißt die Setter-Methode anders, entsteht eine zweite Eigenschaft, und die erste bleibt ohne Setter:

@stand.setter baut eine neue property aus der alten plus dem Setter und weist sie dem Namen der Setter-Methode zu. Heißt der anders, bleibt stand unverändert.

Es geht auch ohne Dekorator: stand = property(fget, fset, fdel, doc).

Schritt 3: Eingebaute Klassen beerben (Ziel 1.8)

Du kannst von list, dict, str, int erben. Die Falle: Eingebaute Methoden rufen intern oft deine überschriebenen Methoden nicht auf.

Bei dict ruft update nicht __setitem__ auf, und auch der Konstruktor mit Schlüsselwörtern nicht. Sicherer sind collections.UserList, UserDict, UserString: Sie arbeiten intern über normale Python-Methoden, deine Überschreibung greift überall.

Zwei weitere Punkte aus der Quelle:

  • __missing__: Eine dict-Unterklasse kann es definieren. Es wird bei d[key] für fehlende Schlüssel aufgerufen. d.get(key) benutzt es nicht.
  • Unveränderbare Typen (int, str, tuple) brauchen __new__ statt __init__, weil der Wert schon bei der Erzeugung feststeht.

Die Ausgabe {'a': 1} zeigt: __missing__ liefert nur einen Wert, es speichert nichts. Und Sieben(2) + 1 ist wieder ein normales int.

Falle: die Prüfungs- und Praxisklassiker

  1. Abstrakt, aber trotzdem instanziierbar: @abstractmethod ohne ABC, oder ein Fehler in der Unterklasse, die nicht alle Methoden überschreibt.
  2. Property: self._x = ... im __init__ umgeht den Setter, und ein abweichender Setter-Name erzeugt eine zweite Eigenschaft.
  3. list/dict beerben: extend, update, der Konstruktor umgehen deine Überschreibung. Nimm UserList/UserDict.

Übungen

Übung 1: Welche Klassen lassen sich instanziieren?

from abc import ABC, abstractmethod

class Basis(ABC):
    @abstractmethod
    def a(self): ...
    @abstractmethod
    def b(self): ...

class K1(Basis):
    def a(self): return 1

class K2(K1):
    def b(self): return 2

class K3(Basis):
    a = 1
    b = 2

class Lose:
    @abstractmethod
    def x(self): ...

class Lose2(ABC):
    def x(self): ...

Trage die alphabetisch sortierte Liste der Klassennamen ein, die sich ohne Fehler instanziieren lassen (Klasse() wirft keinen TypeError). Zur Auswahl: Basis, K1, K2, K3, Lose, Lose2.

Für jede Klasse zwei Fragen: Hat sie ABC in ihrer Vererbungskette? Wenn ja: Bleibt am Ende noch eine Methode übrig, die als abstrakt markiert ist und nicht ersetzt wurde? Beachte, was K2 von K1 erbt, und was ein Klassenattribut gleichen Namens bewirkt.

antwort = ["K2", "K3", "Lose", "Lose2"]
antwort

Übung 2: Eine Klasse mit Property schreiben

Schreibe die Klasse Preis:

  • Preis(250) speichert einen Betrag in ganzen Cent. Die Eigenschaft cent hat Getter und Setter. Der Setter akzeptiert nur int mit Wert >= 0, sonst ValueError.
  • Die Prüfung gilt auch bei der Erzeugung: Preis(-1) löst ValueError aus.
  • Die Eigenschaft euro ist nur lesbar und liefert cent / 100. Eine Zuweisung an euro löst AttributeError aus.
  • Nach einem abgelehnten Wert bleibt der alte Wert erhalten.

Wie legst du im __init__ den Wert so ab, dass die Prüfung auch dort läuft (Schritt 2)? Der Getter braucht ein internes Attribut mit anderem Namen als die Property. Und was bewirkt es, bei euro einfach keinen Setter anzulegen?

class Preis:
    def __init__(self, cent):
        self.cent = cent

    @property
    def cent(self):
        return self._cent

    @cent.setter
    def cent(self, wert):
        if not isinstance(wert, int) or wert < 0:
            raise ValueError("cent muss ein int >= 0 sein")
        self._cent = wert

    @property
    def euro(self):
        return self._cent / 100

Preis

Übung 3: Ein dict beerben, ohne dass Schreibzugriffe durchrutschen

Die Liste protokoll existiert schon. Die Klasse ProtokollDict soll sich wie ein normales dict verhalten. Zusätzlich hängt jedes Schreiben eines Schlüssels den Schlüssel an protokoll an: bei d[k] = v, bei d.update(...) (mit Dict oder mit Schlüsselwörtern) und bei der Erzeugung ProtokollDict(a=1). Lesen, len, in und dict(d) müssen funktionieren.

Die Vorlage erfasst d[k] = v, aber probiere im Kopf (oder in einer Beispielzelle) aus, was bei update und bei ProtokollDict(a=1) passiert. Schritt 3 nennt zwei Wege: die gefährdeten Methoden selbst abfangen, oder eine Basisklasse wählen, bei der alles über __setitem__ läuft.

from collections import UserDict

class ProtokollDict(UserDict):
    def __setitem__(self, key, value):
        protokoll.append(key)
        super().__setitem__(key, value)

ProtokollDict

Merksatz und Prüfstein

Merksatz: Abstrakt ist nur, was ABC und @abstractmethod zusammen sagen, ein Setter prüft nur, wenn du ihn auch im __init__ benutzt, und eingebaute Typen umgehen deine Überschreibungen, außer du nimmst UserDict und Co.

Prüfstein (offene Frage): Eine Property preis hat einen Setter, der negative Werte ablehnt. Trotzdem lässt sich Artikel(-5) erzeugen. Nenne zwei mögliche Ursachen im Code und erkläre, wie du jeweils den Fehler findest.

Quelle: quellen/python-glossar-pcap-pcpp1.md, PCPP1 Block 1, Abschnitte 1.6 (Abstrakte Klassen und Methoden), 1.7 (Kapselung mit Properties), 1.8 (Eingebaute Klassen beerben) und “Typische Fallen in Block 1”. Blockgewicht und Fragenzahl laut Quelle (Stand 1. Oktober 2026, bitte prüfen). Hinweis: das Ersetzen abstrakter Methoden durch Klassenattribute, property.fset und dass dict.get kein __missing__ nutzt stehen nicht in der Quelle, wurden aber mit Python 3.13 ausgeführt und geprüft (bitte prüfen, ob sie in der PCPP1 gefragt werden).