Zum Inhalt springen
aviral gupta

// I3.5 · ca. 35 Min. · Aufbau

functools und Dekoratoren

Nach dieser Lektion schreiben Sie Dekoratoren, die jede Funktion umhüllen und ihren Namen behalten, merken wiederholte Aufrufe mit cache und lru_cache, füllen Argumente mit partial vor und falten Listen mit reduce.

Lektion 5 von 6 in I3 Iteration und funktionale Werkzeuge

Danach können Sie

  • Einen Dekorator schreiben, der eine Funktion umhüllt, und Name und Docstring mit functools.wraps erhalten
  • Ergebnisse einer Funktion mit cache und lru_cache merken und wissen, wann nicht
  • Argumente mit partial vorbelegen und ein Iterable mit reduce zu einem Wert falten
  1. Aufwärmen · Aufgabe 1 von 7

    Aufwärmen aus Modul B3: Eine Funktion ist ein Objekt, das Sie in einer Variablen ablegen können. Was gibt das aus?

    def shout(text):
        return text.upper() + "!"
    
    say = shout
    print(say("hi"), say.__name__)
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen: hello wird einmal definiert und einmal aufgerufen. Was gibt das aus?

    def twice(func):
        def wrapper():
            func()
            func()
        return wrapper
    
    @twice
    def hello():
        print("hello", end=" ")
    
    hello()
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie den functools-Dekorator, der Namen und Docstring von func auf den wrapper kopiert.

    import functools
    
    def logged(func):
        @functools.____(func)
        def wrapper(*args, **kwargs):
            print("calling", func.__name__)
            return func(*args, **kwargs)
        return wrapper
    
    @logged
    def area(w, h):
        """Return the area of a rectangle."""
        return w * h
    
    print(area(2, 3), area.__name__, area.__doc__)
    @functools.(func)
  4. Üben · Aufgabe 4 von 7

    calls zählt, wie oft der Rumpf von square läuft. Was gibt das aus?

    from functools import cache
    
    calls = 0
    
    @cache
    def square(n):
        global calls
        calls += 1
        return n * n
    
    square(4)
    square(4)
    square(5)
    print(calls)
  5. Üben · Aufgabe 5 von 7

    partial und reduce sind aus functools importiert. Ordnen Sie jedem Ausdruck seinen Wert zu.

  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Dieser Dekorator nutzt functools.wraps nicht. Was gibt die letzte Zeile aus?

    def logged(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    
    @logged
    def area(w, h):
        """Return the area."""
        return w * h
    
    print(area.__name__, area.__doc__)
  7. Anwenden · Aufgabe 7 von 7

    Kleine Aufgabe. Schreiben Sie einen Dekorator trace, der vor jedem Aufruf -> name(args) und danach <- ergebnis ausgibt und das Ergebnis zurückgibt. Nutzen Sie functools.wraps. Dekorieren Sie add(a, b), rufen Sie add(2, 3) auf und geben Sie add.__name__ und add.__doc__ aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Einem Cache bei der Arbeit zusehen

logged ist ein Dekorator, der jeden Aufruf ausgibt, der die Funktion erreicht. fib hat zwei Dekoratoren: außen @functools.cache, innen @logged. Das Protokoll zeigt also nur die Aufrufe, die der Cache nicht beantworten konnte. Die zweite Hälfte baut mit partial einen Binär-Parser und multipliziert mit reduce eine Liste. Callable[..., Any] ist die Annotation für „irgendeine Funktion“.

main.py

import functools
from collections.abc import Callable
from typing import Any


def logged(func: Callable[..., Any]) -> Callable[..., Any]:
    """Print every call of func with its arguments."""

    @functools.wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        print("call", func.__name__, args)
        return func(*args, **kwargs)

    return wrapper


@functools.cache
@logged
def fib(n: int) -> int:
    """Return the n-th Fibonacci number."""
    return n if n < 2 else fib(n - 1) + fib(n - 2)


print(fib(5))
print(fib(6))
print(fib.cache_info())
print(fib.__name__, "-", fib.__doc__)

parse_binary = functools.partial(int, base=2)
print(parse_binary("1010"), parse_binary("111"))

product = functools.reduce(lambda acc, n: acc * n, [1, 2, 3, 4, 5], 1)
print(product)

Ausführen mit

python main.py

Ausgabe

call fib (5,)
call fib (4,)
call fib (3,)
call fib (2,)
call fib (1,)
call fib (0,)
5
call fib (6,)
8
CacheInfo(hits=5, misses=7, maxsize=None, currsize=7)
fib - Return the n-th Fibonacci number.
10 7
120
  • Jedes n von 5 bis 0 wurde einmal berechnet; jedes wiederholte fib(n) kam aus dem Cache und wurde nicht protokolliert.
  • fib(6) brauchte nur einen neuen Aufruf, weil fib(5) und fib(4) schon gespeichert waren.
  • fib behielt dank functools.wraps durch beide Dekoratoren Namen und Docstring.
  • parse_binary ist int mit bereits gesetztem base=2 und nimmt deshalb nur den String.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Übungen

Übung 1 von 3

Ein schreiender Dekorator

Vervollständigen Sie den Dekorator shout. Er umhüllt eine Funktion, die einen String liefert, und macht das Ergebnis groß. Der wrapper muss alle Positions- und Schlüsselwortargumente weitergeben, und die dekorierte Funktion muss Namen und Docstring behalten. greet("ada") liefert dann "HELLO, ADA!".

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Rufen Sie das Original auf und ändern Sie, was es zurückgibt: func(*args, **kwargs).upper().

  2. Hinweis 2

    Setzen Sie @functools.wraps(func) in die Zeile über def wrapper.

  3. Hinweis 3

    wrapper nimmt *args und **kwargs und gibt beide unverändert weiter, so passt er zu jeder Funktion.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import functools
from collections.abc import Callable
from typing import Any


def shout(func: Callable[..., str]) -> Callable[..., str]:
    """Make the string result of func upper case."""

    @functools.wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> str:
        return func(*args, **kwargs).upper()

    return wrapper


@shout
def greet(name: str, punctuation: str = "!") -> str:
    """Greet someone by name."""
    return f"hello, {name}{punctuation}"


if __name__ == "__main__":
    print(greet("ada"))
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

import functools
from collections.abc import Callable
from typing import Any


def shout(func: Callable[..., str]) -> Callable[..., str]:
    """Make the string result of func upper case."""

    def wrapper(*args: Any, **kwargs: Any) -> str:
        return func(*args, **kwargs)

    return wrapper


@shout
def greet(name: str, punctuation: str = "!") -> str:
    """Greet someone by name."""
    return f"hello, {name}{punctuation}"


if __name__ == "__main__":
    print(greet("ada"))

test_main.py

from main import greet, shout


def test_upper():
    """greet('ada') wird groß geschrieben"""
    got = greet("ada")
    assert got == "HELLO, ADA!", f"greet('ada') lieferte {got!r}, erwartet: 'HELLO, ADA!'"


def test_keyword_argument():
    """Schlüsselwortargumente erreichen die Funktion"""
    got = greet("bo", punctuation="?")
    assert got == "HELLO, BO?", f"greet('bo', punctuation='?') lieferte {got!r}, erwartet: 'HELLO, BO?'"


def test_name_and_docstring():
    """greet behält Namen und Docstring"""
    got = greet.__name__, greet.__doc__
    assert got == ("greet", "Greet someone by name."), f"greet.__name__ und __doc__ sind {got!r}: Nutzen Sie functools.wraps"


def test_other_function():
    """shout funktioniert mit jeder Funktion, die einen String liefert"""

    @shout
    def join(*words):
        return "-".join(words)

    got = join("a", "b")
    assert got == "A-B", f"ein dekoriertes join('a', 'b') lieferte {got!r}, erwartet: 'A-B'"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 2 von 3

Gitterwege zählen mit Cache

count_paths(rows, cols) zählt die Wege durch ein Gitter mit rows Schritten nach unten und cols nach rechts, nur nach unten oder rechts. Die Funktion stimmt, berechnet aber dieselben kleineren Gitter immer wieder. Fügen Sie @cache aus functools hinzu, damit jedes Gitter einmal berechnet wird. Die Tests rufen count_paths.cache_info() auf, das nur eine gecachte Funktion hat.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    Ein Dekorator steht direkt in der Zeile über def.

  2. Hinweis 2

    cache ist schon importiert, die Zeile lautet also einfach @cache.

  3. Hinweis 3

    Die Rekursion ruft den Namen count_paths auf, der nach dem Dekorieren die gecachte Version ist. Also werden auch die inneren Aufrufe gecacht.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from functools import cache


@cache
def count_paths(rows: int, cols: int) -> int:
    """Count the paths through a grid, moving only right or down."""
    if rows == 0 or cols == 0:
        return 1
    return count_paths(rows - 1, cols) + count_paths(rows, cols - 1)


if __name__ == "__main__":
    print(count_paths(2, 2))
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from functools import cache


def count_paths(rows: int, cols: int) -> int:
    """Count the paths through a grid, moving only right or down."""
    if rows == 0 or cols == 0:
        return 1
    return count_paths(rows - 1, cols) + count_paths(rows, cols - 1)


if __name__ == "__main__":
    print(count_paths(2, 2))

test_main.py

from main import count_paths


def test_small_grids():
    """Ein 1-mal-1-Gitter hat 2 Wege, ein 2-mal-2-Gitter 6"""
    got = count_paths(1, 1), count_paths(2, 2)
    assert got == (2, 6), f"count_paths(1, 1) und count_paths(2, 2) lieferten {got!r}, erwartet: (2, 6)"


def test_straight_line():
    """Ohne Schritte nach unten gibt es einen Weg"""
    got = count_paths(0, 5)
    assert got == 1, f"count_paths(0, 5) lieferte {got!r}, erwartet: 1"


def test_bigger_grid():
    """Ein 10-mal-10-Gitter hat 184756 Wege"""
    got = count_paths(10, 10)
    assert got == 184756, f"count_paths(10, 10) lieferte {got!r}, erwartet: 184756"


def test_is_cached():
    """count_paths ist gecacht und nutzt Ergebnisse wieder"""
    assert hasattr(count_paths, "cache_info"), "count_paths hat kein cache_info(): Setzen Sie @cache über def count_paths"
    count_paths(3, 3)
    hits = count_paths.cache_info().hits
    assert hits > 0, f"der Cache meldet {hits} Treffer nach count_paths(3, 3)"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Übung 3 von 3

partial und reduce

Schreiben Sie zwei Funktionen. parse_base(base) gibt eine Funktion zurück, die einen String als Zahl in dieser Basis liest: parse_base(16)("ff") ist 255. Nutzen Sie partial mit int. merge_all(dicts) führt eine Liste von Dicts von links nach rechts zusammen, spätere Werte gewinnen, mit reduce und dem Operator |: [{"a": 1}, {"a": 3, "b": 2}] ergibt {"a": 3, "b": 2}. Eine leere Liste ergibt {}.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.

Hinweise
  1. Hinweis 1

    partial(int, base=base) ist int mit bereits gesetztem Schlüsselwort base.

  2. Hinweis 2

    a | b bildet ein neues Dict aus a und b, wobei die Werte von b gewinnen. reduce wendet das paarweise von links an.

  3. Hinweis 3

    Geben Sie reduce ein leeres Dict als Startwert, damit eine leere Liste {} liefert: reduce(lambda merged, d: merged | d, dicts, empty).

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

from collections.abc import Callable
from functools import partial, reduce


def parse_base(base: int) -> Callable[[str], int]:
    """Return a function that reads a string as a number in base."""
    return partial(int, base=base)


def merge_all(dicts: list[dict[str, int]]) -> dict[str, int]:
    """Merge dicts from left to right; later values win."""
    empty: dict[str, int] = {}
    return reduce(lambda merged, d: merged | d, dicts, empty)


if __name__ == "__main__":
    print(parse_base(16)("ff"))
    print(merge_all([{"a": 1}, {"a": 3, "b": 2}]))
Auf dem eigenen Computer ausführen

Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.py

from collections.abc import Callable
from functools import partial, reduce


def parse_base(base: int) -> Callable[[str], int]:
    """Return a function that reads a string as a number in base."""
    return int


def merge_all(dicts: list[dict[str, int]]) -> dict[str, int]:
    """Merge dicts from left to right; later values win."""
    return {}


if __name__ == "__main__":
    print(parse_base(16)("ff"))
    print(merge_all([{"a": 1}, {"a": 3, "b": 2}]))

test_main.py

from main import merge_all, parse_base


def test_parse_base():
    """Basis 2 liest 101 als 5, Basis 16 liest ff als 255"""
    got = parse_base(2)("101"), parse_base(16)("ff")
    assert got == (5, 255), f"parse_base(2)('101') und parse_base(16)('ff') lieferten {got!r}, erwartet: (5, 255)"


def test_merge():
    """Spätere Dicts gewinnen"""
    got = merge_all([{"a": 1}, {"b": 2}, {"a": 3}])
    assert got == {"a": 3, "b": 2}, f"merge_all lieferte {got!r}, erwartet: {{'a': 3, 'b': 2}}"


def test_merge_empty():
    """Ohne Dicts kommt ein leeres Dict"""
    got = merge_all([])
    assert got == {}, f"merge_all([]) lieferte {got!r}, erwartet: {{}}"


def test_inputs_unchanged():
    """merge_all ändert die übergebenen Dicts nicht"""
    first = {"a": 1}
    merge_all([first, {"a": 2}])
    assert first == {"a": 1}, f"das erste Dict wurde zu {first!r} geändert"

Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.

Programm ausführen:

python main.py

Prüfungen ausführen (learnrun.py muss im selben Ordner liegen):

python learnrun.py test
learnrun.py herunterladen

Häufige Fehler

Ein Dekorator, der den wrapper nicht zurückgibt

def logged(func):
    def wrapper(*args, **kwargs):
        print("calling", func.__name__)
        return func(*args, **kwargs)


@logged
def greet(name):
    return "hi " + name


print(greet("Ada"))

Was Python ausgibt

TypeError: 'NoneType' object is not callable

Warum, und die Lösung

logged definiert wrapper, gibt ihn aber nie zurück. Also liefert logged None, und @logged setzt greet = None. Schreiben Sie return wrapper als letzte Zeile des Dekorators, auf derselben Einrückung wie def wrapper.

Eine gecachte Funktion mit einer Liste aufrufen

from functools import cache


@cache
def total(prices):
    return sum(prices)


print(total([1, 2, 3]))

Was Python ausgibt

TypeError: unhashable type: 'list'

Warum, und die Lösung

Der Cache ist ein Dict mit den Argumenten als Schlüssel, und eine Liste kann kein Dict-Schlüssel sein, weil sie veränderbar ist. Übergeben Sie stattdessen ein Tupel, total((1, 2, 3)), oder lassen Sie die Funktion ungecacht.

reduce auf einer leeren Liste ohne Startwert

from functools import reduce

print(reduce(lambda a, b: a + b, []))

Was Python ausgibt

TypeError: reduce() of empty iterable with no initial value

Warum, und die Lösung

Ohne Elemente und ohne Startwert hat reduce nichts zurückzugeben. Geben Sie den Startwert als drittes Argument an: reduce(lambda a, b: a + b, [], 0) ergibt 0.

Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein Dekorator umhüllt eine Funktion

Eine Funktion ist ein Objekt: Sie können sie einer anderen Funktion übergeben und eine neue zurückbekommen. Ein Dekorator nimmt eine Funktion und gibt einen Ersatz zurück, meist eine innere wrapper(*args, **kwargs), die vor oder nach dem Original etwas tut. @logged über def area ist dasselbe wie area = logged(area) danach. Dekorieren Sie wrapper mit @functools.wraps(func): Sonst heißt area.__name__ "wrapper" und der Docstring fehlt, was help(), Tracebacks und Tests verwirrt.

cache und lru_cache merken sich Ergebnisse

@cache speichert jedes Ergebnis unter seinen Argumenten. Ein zweiter Aufruf mit denselben Argumenten liefert das gespeicherte Ergebnis, ohne den Rumpf auszuführen. Das passt zu Funktionen, deren Ergebnis nur von den Argumenten abhängt, etwa einem rekursiven Fibonacci, das so von exponentieller auf lineare Zeit fällt. @lru_cache(maxsize=128) behält nur die zuletzt genutzten Ergebnisse. Die Argumente müssen hashbar sein, eine Liste löst TypeError aus. Cachen Sie nie Funktionen, die Uhr, Dateien oder input() lesen. cache_info() zeigt Treffer und Fehlschläge.

partial und reduce

partial(func, *args, **kwargs) gibt ein neues aufrufbares Objekt zurück, bei dem einige Argumente schon feststehen: partial(int, base=2)("101") ist 5. Das hilft, wo eine API eine Funktion mit einem Argument erwartet, etwa bei key= oder einem Callback. reduce(function, iterable, initial) faltet ein Iterable von links zu einem Wert: reduce(f, [a, b, c]) ist f(f(a, b), c). Geben Sie initial an, dann liefert ein leeres Iterable ein Ergebnis statt TypeError. Für Summen nehmen Sie sum; reduce ist für andere Faltungen, etwa das Zusammenführen von Dicts.

Quellen

Zuletzt geprüft am 29. September 2026