Das functools-Modul in Python: lru_cache, partial und reduce

Das functools-Modul bündelt Werkzeuge, die Funktionen erweitern, zwischenspeichern und umbauen. Wir schauen uns lru_cache, partial, reduce und wraps an – mit lauffähigen Beispielen.

Teilen

Funktionen sind in Python Objekte erster Klasse – du kannst sie herumreichen, verpacken und umbauen wie jeden anderen Wert. Das functools-Modul aus der Standardbibliothek nutzt genau das aus: Es bündelt Werkzeuge, die bestehende Funktionen erweitern, ihre Ergebnisse zwischenspeichern oder ihre Signatur umbauen. In diesem Beitrag schauen wir uns die wichtigsten Bausteine an – alle sofort einsatzbereit, ohne externe Pakete.

lru_cache: Ergebnisse automatisch zwischenspeichern

Der wohl bekannteste Helfer ist lru_cache. Er merkt sich die Rückgabewerte einer Funktion pro Argumentkombination. Wird die Funktion mit denselben Argumenten erneut aufgerufen, kommt das Ergebnis direkt aus dem Cache statt neu berechnet zu werden. LRU steht für Least Recently Used: Ist der Cache voll, fliegt der am längsten nicht mehr genutzte Eintrag raus.

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(40))          # 102334155 – in Sekundenbruchteilen
print(fib.cache_info()) # CacheInfo(hits=38, misses=41, maxsize=None, currsize=41)

Ohne Cache würde fib(40) zu Millionen redundanter Aufrufe führen. Mit dem Dekorator wird jeder Wert nur einmal berechnet. Über fib.cache_info() siehst du Treffer und Fehlschläge, mit fib.cache_clear() leerst du den Cache wieder.

Wichtig: Die Argumente müssen hashbar sein (also z. B. Zahlen, Strings oder Tupel – keine Listen). Und die Funktion sollte für gleiche Eingaben immer dasselbe zurückgeben. Seit Python 3.9 gibt es zusätzlich @cache als Kurzform für @lru_cache(maxsize=None).

partial: Argumente vorab festlegen

Mit partial baust du aus einer bestehenden Funktion eine neue, bei der einzelne Argumente schon fest verdrahtet sind. Das spart Wiederholung und macht Code lesbarer, besonders bei Callbacks.

from functools import partial

def potenz(basis, exponent):
    return basis ** exponent

quadrat = partial(potenz, exponent=2)
kubik = partial(potenz, exponent=3)

print(quadrat(5))  # 25
print(kubik(2))    # 8

Ein praktisches Beispiel: Du willst eine Liste von Strings ins Dezimalsystem und ins Hexadezimalsystem umwandeln. Statt jedes Mal die base anzugeben, erzeugst du spezialisierte Varianten von int:

from functools import partial

als_hex = partial(int, base=16)
als_binaer = partial(int, base=2)

print(als_hex("ff"))     # 255
print(als_binaer("101")) # 5

reduce: Eine Sequenz auf einen Wert zusammenfalten

reduce wendet eine Funktion mit zwei Argumenten fortlaufend auf die Elemente einer Sequenz an und faltet sie so zu einem einzigen Ergebnis zusammen. Für Summen oder Maxima gibt es zwar eingebaute Funktionen, aber bei benutzerdefinierten Verknüpfungen ist reduce genau richtig.

from functools import reduce

zahlen = [2, 3, 4, 5]

# Produkt aller Zahlen: ((2*3)*4)*5
produkt = reduce(lambda a, b: a * b, zahlen)
print(produkt)  # 120

# Mit Startwert – nützlich bei leeren Listen
summe = reduce(lambda a, b: a + b, zahlen, 100)
print(summe)  # 114

Ein Tipp aus der Praxis: Wenn eine einfache Schleife oder eine eingebaute Funktion wie sum den Zweck erfüllt, sind diese oft lesbarer. reduce glänzt bei nicht-trivialen Faltungen, etwa dem Zusammenführen mehrerer Dictionaries.

wraps: Metadaten beim Dekorieren bewahren

Wenn du eigene Dekoratoren schreibst, ersetzt die innere Wrapper-Funktion die Originalfunktion – inklusive Name und Docstring. Das erschwert Debugging und stört Werkzeuge, die auf diese Metadaten zugreifen. wraps kopiert die wichtigen Attribute zurück auf den Wrapper.

from functools import wraps

def protokolliere(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Rufe {func.__name__} auf")
        return func(*args, **kwargs)
    return wrapper

@protokolliere
def gruesse(name):
    "Gibt eine Begruessung aus."
    return f"Hallo {name}"

print(gruesse.__name__)  # gruesse (statt 'wrapper')
print(gruesse.__doc__)   # Gibt eine Begruessung aus.

Ohne @wraps würde gruesse.__name__ plötzlich "wrapper" zurückgeben. Deshalb gehört wraps in praktisch jeden selbstgeschriebenen Dekorator.

singledispatch: Funktionen nach Typ überladen

Python kennt kein klassisches Überladen von Funktionen. singledispatch holt das nach: Du registrierst je nach Typ des ersten Arguments eine eigene Implementierung. Das ist sauberer als eine lange Kette von if isinstance(...).

from functools import singledispatch

@singledispatch
def beschreibe(wert):
    return f"Unbekannter Typ: {wert!r}"

@beschreibe.register
def _(wert: int):
    return f"Ganzzahl mit Wert {wert}"

@beschreibe.register
def _(wert: list):
    return f"Liste mit {len(wert)} Elementen"

print(beschreibe(42))        # Ganzzahl mit Wert 42
print(beschreibe([1, 2, 3])) # Liste mit 3 Elementen
print(beschreibe("hi"))      # Unbekannter Typ: 'hi'

Fazit

Das functools-Modul ist eine kleine Werkzeugkiste, die deinen Alltag spürbar erleichtert: lru_cache beschleunigt teure Berechnungen fast ohne Aufwand, partial reduziert Wiederholung, reduce faltet Sequenzen zusammen, wraps hält deine Dekoratoren sauber und singledispatch bringt typbasiertes Überladen. Am besten lernst du diese Helfer kennen, indem du sie in einem eigenen Projekt ausprobierst – such dir eine langsame Funktion und setz ein @lru_cache davor. Der Unterschied ist oft verblüffend.