PCPP1 Block 1 (Teil 2a): *args/**kwargs, Dekoratoren, static/classmethod
Track Python · PCPP1 Block 1 (Ziele 1.4 und 1.5) · ca. 40 Min.
Worum es geht
Fortgeschrittene OOP (advanced object-oriented programming) ist laut Quelle der größte PCPP1-Block: 15 von 45 Fragen, 35 % (Stand der Quelle: 1. Oktober 2026, bitte vor der Buchung prüfen). Dieser Teil deckt die Ziele 1.4 und 1.5 ab: Dekoratoren (decorators) mit Argumenten, gestapelt und als Klasse, dazu @classmethod und @staticmethod. Als JS/TS-Entwickler kennst du Higher-Order-Functions und static. Neu sind die Details, an denen Prüfungsfragen ansetzen.
Lektion 01 hat Dekoratoren einfach behandelt (@deco ist f = deco(f), functools.wraps). Das wird hier nicht wiederholt, sondern vertieft: drei Ebenen, Stapel-Reihenfolge, Klassen als Dekorator. Nicht behandelt (kommt später): Klassendekorator-Beispiele über das eine Quellbeispiel hinaus und Zustand pro Instanz bei Klassen-Dekoratoren auf Methoden mit __get__.
Ablauf: Tabelle JS/TS gegen Python, zwei Schritte mit lauffähigem Code, drei Übungen. In Teil 2b folgen abstrakte Klassen, Properties und das Beerben eingebauter Typen.
Von JS/TS her gedacht
| Thema | JS/TS | Python |
|---|---|---|
| Funktion umhüllen | Higher-Order-Function: const g = deco(f) |
@deco über def f, gleichbedeutend mit f = deco(f) |
| Parametrisierter Wrapper | deco(3)(f) |
@deco(3): drei Ebenen (Fabrik, Dekorator, Wrapper) |
| Argumente durchreichen | (...args) => fn(...args) |
def wrapper(*args, **kwargs): return fn(*args, **kwargs) |
static-Methode |
static f() |
@staticmethod (weder self noch cls) |
| Alternative Konstruktoren | static fromString() mit new this(...) |
@classmethod mit cls(...) |
Konzept in kleinen Schritten
Schritt 1: Dekoratoren vertieft (Ziel 1.4)
Gestapelte Dekoratoren (stacked decorators). Bei @a über @b über def f gilt f = a(b(f)). Der untere Dekorator wird zuerst angewendet. Beim Aufruf läuft aber der obere Wrapper zuerst, weil er außen liegt. Es gibt also zwei Zeitpunkte mit entgegengesetzter Reihenfolge:
Erwartete Ausgabe: b wird angewendet, a wird angewendet, --- Aufruf, a vor, b vor, f, b nach, a nach. Merkhilfe: Anwenden von unten nach oben, Ausführen von außen nach innen und wieder zurück.
Dekorator mit Argumenten (decorator factory). @mal(3) bedeutet f = mal(3)(f). Der Ausdruck mal(3) wird zuerst ausgewertet und muss einen Dekorator liefern. Darum drei Ebenen:
Wo ein Zähler oder anderer Zustand liegt, entscheidet, wer ihn teilt. Steht er in Ebene 1, teilen ihn alle Funktionen, die mit demselben mal(3)-Objekt dekoriert wurden. Steht er in Ebene 2, hat jede dekorierte Funktion ihren eigenen. Das ist Übung 2.
Klasse als Dekorator. Eine Klasse mit __init__(self, fn) und __call__(self, *args, **kwargs) funktioniert, weil ein Objekt mit __call__ aufrufbar (callable) ist. Der Vorteil: Der Zustand lebt als Attribut am Objekt und ist von außen lesbar. Damit Name und Docstring erhalten bleiben, nimmst du functools.update_wrapper(self, fn) (die Funktionsform von wraps, steht nicht in der Quelle, bitte prüfen, ob die Prüfung sie fragt):
Ein Haken: Auf einer Methode funktioniert ein Klassendekorator dieser Form nicht. Das Objekt Zaehlt(...) ist keine Funktion und wird deshalb nicht zur gebundenen Methode, self wird nicht übergeben:
Klassendekorator (class decorator) ist etwas anderes: eine Funktion, die eine Klasse bekommt und eine Klasse zurückgibt, etwa um Attribute oder Methoden anzuhängen. @add_hello über class Foo entspricht Foo = add_hello(Foo). Verwechsle die beiden Begriffe nicht: “Klasse als Dekorator” dekoriert Funktionen mit einer Klasse, “Klassendekorator” dekoriert Klassen mit einer Funktion.
Schritt 2: @classmethod und @staticmethod (Ziel 1.5)
@classmethod: erster Parameter ist die Klasse (cls). Bei Vererbung ist es die tatsächlich aufgerufene Klasse, auch wenn die Methode in der Basisklasse steht. Darum eignet sie sich für alternative Konstruktoren (return cls(...)).@staticmethod: keinself, keincls. Eine normale Funktion im Namensraum der Klasse.- Beide lassen sich über die Klasse und über eine Instanz aufrufen.
Beachte Tier.anzahl und Hund.anzahl: type(self).anzahl += 1 liest den Wert der Basisklasse (0), addiert 1 und schreibt ihn als neues Klassenattribut an Hund. Tier.anzahl bleibt 0. Das ist dieselbe Mechanik wie bei self.x += 1 in Lektion 07, nur eine Ebene höher. Wer die Summe über alle Unterklassen will, schreibt Tier.anzahl += 1 fest hin.
Falle: die Prüfungs- und Praxisklassiker
- Stapel-Reihenfolge: angewendet wird von unten nach oben, ausgeführt von außen (oben) nach innen.
@deco(3)ohne die dritte Ebene (Fabrik vergessen):deco(3)bekommt dann die Funktion als Argumentn, und alles ist verdreht.wrapsundreturnvergessen: Name und Docstring weg, RückgabewertNone(Lektion 01).@classmethodbei Vererbung:clsist die aufgerufene Unterklasse, undtype(self).x += 1legt ein neues Attribut an der Unterklasse an.
Übungen
Übung 1: Gestapelte Dekoratoren vorhersagen
Der Code steht fest. Du sagst vorher, was danach in der Liste spur steht:
spur = []
def markiere(name):
def deco(fn):
spur.append("dekoriert " + name)
def wrapper(*args, **kwargs):
spur.append("vor " + name)
r = fn(*args, **kwargs)
spur.append("nach " + name)
return r
return wrapper
return deco
@markiere("a")
@markiere("b")
def f():
spur.append("f")
f()Trage den Inhalt von spur als Liste ein (sieben Einträge).
Trenne zwei Zeitpunkte. Beim Ausführen von def (zwei Einträge): Welcher Dekorator bekommt f zuerst? Beim Aufruf f(): Welcher Wrapper liegt außen, und was passiert vor und nach dem inneren Aufruf?
antwort = ["dekoriert b", "dekoriert a", "vor a", "vor b", "f", "nach b", "nach a"]
antwortÜbung 2: Dekorator mit Argumenten schreiben
Schreibe begrenzt(n). Eine damit dekorierte Funktion darf höchstens n Mal aufgerufen werden. Der Aufruf Nummer n + 1 (und alle weiteren) löst RuntimeError aus, ohne die Funktion auszuführen. Anforderungen:
- Argumente (positionell und per Keyword) und Rückgabewert werden durchgereicht.
- Name und Docstring der Funktion bleiben erhalten.
- Jede dekorierte Funktion zählt für sich, auch wenn mehrere mit demselben Objekt
begrenzt(2)dekoriert werden. begrenzt(0)erlaubt keinen einzigen Aufruf.
Drei Ebenen (Schritt 1). Überlege, in welcher Ebene der Zähler leben muss, damit zwei Funktionen ihn nicht teilen, und wie der Wrapper ihn verändern darf, obwohl er in einer äußeren Funktion liegt (Lektion 01). Und was muss passieren, bevor die echte Funktion aufgerufen wird?
import functools
def begrenzt(n):
def deco(fn):
aufrufe = 0
@functools.wraps(fn)
def wrapper(*args, **kwargs):
nonlocal aufrufe
if aufrufe >= n:
raise RuntimeError("Limit erreicht")
aufrufe += 1
return fn(*args, **kwargs)
return wrapper
return deco
begrenztÜbung 3: classmethod und Klassenzähler vorhersagen
class Tier:
anzahl = 0
def __init__(self, name):
self.name = name
type(self).anzahl += 1
@classmethod
def aus_text(cls, s):
return cls(s.strip().title())
@staticmethod
def laut(s):
return s.upper()
class Hund(Tier):
pass
h = Hund.aus_text(" rex ")
t = Tier("x")Trage das Tupel (type(h).__name__, h.name, Tier.anzahl, Hund.anzahl) ein.
Welche Klasse bekommt aus_text als cls, wenn du es über Hund aufrufst? Und type(self).anzahl += 1 ist eine Zuweisung: Auf welche Klasse schreibt sie bei einem Hund, auf welche bei einem Tier? Verfolge beide Zähler getrennt.
antwort = ("Hund", "Rex", 1, 1)
antwortMerksatz und Prüfstein
Merksatz: Dekoratoren wendest du von unten nach oben an und führst sie von außen nach innen aus, @deco(3) heißt drei Ebenen, und cls ist die aufgerufene Unterklasse.
Prüfstein (offene Frage): Du schreibst @begrenzt(3) und plötzlich teilen sich alle dekorierten Funktionen einen Zähler. In welcher der drei Ebenen liegt der Fehler, und wie findest du ihn? Und warum bleibt Tier.anzahl bei Hunden auf 0, wenn __init__ type(self).anzahl += 1 schreibt?
Weiter geht es mit Teil 2b: Abstrakte Klassen, Properties, Builtins beerben.
Quelle: quellen/python-glossar-pcap-pcpp1.md, PCPP1 Block 1, Abschnitte 1.4 (Erweiterte Argumente und Dekoratoren), 1.5 (Statische und Klassenmethoden) und “Typische Fallen in Block 1”. Blockgewicht und Fragenzahl laut Quelle (Stand 1. Oktober 2026, bitte prüfen). Hinweis: functools.update_wrapper, das Verhalten eines Klassendekorators auf Methoden und type(self).x += 1 bei Unterklassen 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).