Zum Inhalt springen
JAY IMDAHL
App-Entwicklung 9 min

Python Closures: Funktionen mit Gedächtnis

Closures sind das Geheimnis hinter Decorators, Factories und elegantem State-Handling in Python. Ein praxisnaher Deep-Dive: wie sie funktionieren und wann du sie brauchst.

Closures einfach erklärt Thumbnail

Wer länger Python schreibt, stolpert irgendwann über Code wie diesen:

def erzeuge_multiplikator(faktor):
    def multipliziere(x):
        return x * faktor
    return multipliziere

verdoppeln = erzeuge_multiplikator(2)
verdreifachen = erzeuge_multiplikator(3)

Eine Funktion gibt eine Funktion zurück, und die behält den Wert von faktor, obwohl erzeuge_multiplikator() längst durchgelaufen ist. Genau das ist eine Closure: eine Funktion, die sich Variablen aus ihrer Umgebung merkt.

In diesem Post schauen wir uns an, wie Closures intern funktionieren, wann du sie wirklich brauchst, und welche Fallen du kennen solltest. Am Ende bauen wir einen kleinen Rate-Limiter, der das alles in der Praxis zeigt.

1. Das Problem hinter Closures

Bevor wir uns Closures genauer ansehen, hilft die Frage: Was wäre eigentlich, wenn es sie nicht gäbe? Sagen wir, du brauchst einen Zähler, der bei jedem Aufruf hochzählt.

Variante 1: Globale Variable

zaehler = 0

def naechste_zahl():
    global zaehler
    zaehler += 1
    return zaehler

Funktioniert, aber nur einmal. Mehrere Zähler? Brauchst du mehrere Globals. Jeder Codeteil im Programm kann den Zustand kaputtmachen. global ist fast immer ein Code Smell.

Variante 2: Klasse

class Zaehler:
    def __init__(self):
        self.wert = 0

    def naechste(self):
        self.wert += 1
        return self.wert

Sauber, isoliert, beliebig viele Instanzen. Für viele Fälle die richtige Lösung, aber gefühlt overengineered, wenn man eigentlich nur eine Funktion mit Gedächtnis will.

Variante 3: Closure

def zaehler_factory():
    wert = 0
    def naechste():
        nonlocal wert
        wert += 1
        return wert
    return naechste

z1 = zaehler_factory()
z2 = zaehler_factory()
print(z1())  # 1
print(z1())  # 2
print(z2())  # 1

Eigener Zustand pro Aufruf der Factory, kein Global, keine Klasse. Nur eine Funktion, die eine Funktion zurückgibt. Das ist die Nische, die Closures füllen.

2. Was eine Closure ausmacht

In Python entsteht eine Closure automatisch, sobald drei Dinge zusammenkommen:

  1. Eine innere Funktion, die in einer äußeren Funktion definiert ist.
  2. Eine freie Variable: die innere Funktion greift auf einen Namen zu, der weder Parameter noch lokal ist, sondern aus dem äußeren Scope stammt.
  3. Die innere Funktion verlässt den äußeren Scope, meistens per return. Es kann aber auch ein Callback, ein Listen-Eintrag oder ein registrierter Handler sein.

Fehlt eine der drei Bedingungen, hast du keine Closure, sondern nur eine geschachtelte Funktion.

def aussen(x):
    def innen():
        return x * 2     # x ist die freie Variable
    return innen

Du musst nichts dekorieren oder annotieren. Der Interpreter erkennt selbst, dass innen ein Stück Kontext mitnehmen muss, und packt das automatisch ein.

3. Wie Closures intern funktionieren

Damit x aus dem Beispiel oben den Aufruf von aussen() überlebt, kann Python die Variable nicht einfach auf den Stack legen. Der wäre weg, sobald die äußere Funktion zurückkehrt. Stattdessen landet sie in einem Cell-Objekt: ein winziger Container mit genau einem Slot, der den Wert hält.

Die innere Funktion bekommt eine Referenz auf diese Cell mit auf den Weg. Solange die Closure existiert, bleibt auch die Cell am Leben, und damit der Wert.

Das kannst du dir direkt anschauen:

def aussen(x, y):
    def innen():
        return x + y
    return innen

f = aussen(10, 20)

print(f.__closure__[0].cell_contents)  # 10
print(f.__closure__[1].cell_contents)  # 20
print(f.__code__.co_freevars)          # ('x', 'y')

__closure__ enthält die Cells, co_freevars die zugehörigen Namen. Beides zusammen ist die Closure.

Closures speichern Referenzen, keine Kopien

Wichtig: In der Cell liegt eine Referenz, kein Snapshot. Mehrere Closures können sich dieselbe Cell teilen und sehen Änderungen, die eine andere Closure macht.

def erzeuge_box():
    x = 10
    def get():
        return x
    def setze(neuer_wert):
        nonlocal x
        x = neuer_wert
    return get, setze

g, s = erzeuge_box()
print(g())   # 10
s(99)
print(g())   # 99

Das ist nicht nur Theorie. Wir nutzen das gleich, um gekapselten Zustand mit mehreren Operationen zu bauen.

4. nonlocal: Variablen verändern statt nur lesen

Solange du eine eingefangene Variable nur liest, brauchst du nichts zu deklarieren. Sobald du sie aber zuweist, schlägt eine Falle zu:

def zaehler_factory():
    zaehler = 0
    def hoch():
        zaehler = zaehler + 1   # UnboundLocalError!
        return zaehler
    return hoch

Python sieht in hoch() die Zuweisung an zaehler und entscheidet beim Kompilieren: das ist eine lokale Variable. Beim Aufruf hat sie aber noch keinen Wert, also Fehler. Die äußere Variable wird gar nicht erst angefasst.

Das Schlüsselwort nonlocal löst das:

def zaehler_factory():
    zaehler = 0
    def hoch():
        nonlocal zaehler
        zaehler = zaehler + 1
        return zaehler
    return hoch

Damit sagst du Python explizit: “Diese Variable kommt aus dem nächsten umschließenden Funktions-Scope, und ich will sie dort verändern.”

nonlocal ist nicht dasselbe wie global:

SchlüsselwortVerweist aufVerfügbar in
globalModul-EbeneAllen Funktionen
nonlocalUmschließender FunktionGeschachtelten Funktionen

Faustregel: Wenn du in einer inneren Funktion einen UnboundLocalError bekommst und der Name eigentlich aus dem äußeren Scope kommt, hast du nonlocal vergessen.

5. Der Late-Binding-Klassiker

Der berühmteste Closure-Bug. Was gibt dieser Code aus?

funktionen = []
for i in range(3):
    def f():
        return i
    funktionen.append(f)

for f in funktionen:
    print(f())

Die Antwort ist nicht 0, 1, 2, sondern 2, 2, 2. Alle drei Funktionen liefern denselben Wert.

Der Grund liegt in dem, was wir uns gerade über Cells angeschaut haben: Alle drei Closures teilen sich dieselbe Cell für i, weil sie im selben äußeren Scope leben. Die Schleife schreibt nacheinander 0, 1, 2 in diese eine Cell. Aufgerufen werden die Funktionen erst nach der Schleife, also liest jede den letzten Wert.

Das nennt sich Late Binding: Die Closure liest den Wert spät, nämlich beim Aufruf, nicht früh beim Definieren.

Lösung 1: Default-Argument

Default-Werte werden beim Definieren ausgewertet, perfekt zum Einfrieren des aktuellen Stands.

funktionen = []
for i in range(3):
    def f(i=i):              # Default = aktueller Wert von i
        return i
    funktionen.append(f)

Sieht erst seltsam aus, weil links und rechts derselbe Name steht. Links ist es aber der Parameter der inneren Funktion, rechts die Schleifenvariable. Funktioniert.

Lösung 2: Factory-Funktion

Sauberer, weil expliziter:

def erzeuge(i):
    def f():
        return i
    return f

funktionen = [erzeuge(i) for i in range(3)]

Jeder Aufruf von erzeuge(i) hat sein eigenes Frame, seinen eigenen Parameter, seine eigene Cell. Drei verschiedene Closures, drei verschiedene Werte.

Immer wenn du in einer Schleife eine Funktion definierst, die eine Schleifenvariable benutzt, lohnt ein zweiter Blick. Event-Handler, Callbacks, asynchrone Tasks, parametrisierte Tests: überall lauert dieser Bug.

6. Wo Closures in echtem Code auftauchen

Closures sind kein Theoriethema. Sie stecken in idiomatischem Python an vielen Stellen, meistens, ohne dass das Wort Closure irgendwo fällt.

Function Factories

Eine Funktion, die spezialisierte Funktionen baut:

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

quadrat = potenz(2)
kubik = potenz(3)

Memoization

Ein Cache, der zwischen Aufrufen lebt:

def memoize(funktion):
    cache = {}
    def gemerkt(*args):
        if args not in cache:
            cache[args] = funktion(*args)
        return cache[args]
    return gemerkt

cache ist eine freie Variable in gemerkt, sie überlebt zwischen den Aufrufen und füllt sich nach und nach. In der Praxis nimmst du natürlich functools.lru_cache, das genau dasselbe Prinzip in C umsetzt.

Konfigurations-Wrapper

Eine vorkonfigurierte Funktion zurückgeben:

def baue_validator(min_laenge, max_laenge):
    def pruefe(text):
        return min_laenge <= len(text) <= max_laenge
    return pruefe

passwort_ok = baue_validator(8, 64)
username_ok = baue_validator(3, 20)

Gekapselter Zustand mit mehreren Operationen

Wenn du Zustand mit mehreren Operationen brauchst, aber keine Klasse aufbauen willst, gibst du mehrere Closures zurück, die sich denselben Scope teilen:

def neues_konto(start=0):
    saldo = start

    def einzahlen(betrag):
        nonlocal saldo
        saldo += betrag
        return saldo

    def auszahlen(betrag):
        nonlocal saldo
        if betrag > saldo:
            raise ValueError('Nicht genug Guthaben')
        saldo -= betrag
        return saldo

    def stand():
        return saldo

    return einzahlen, auszahlen, stand

saldo ist hier wirklich privat. Von außen kommt da niemand ran, außer über die drei zurückgegebenen Funktionen. Mehr Kapselung als eine normale Klasse mit Underscore-Konvention.

7. Closures vs. Klassen

Closures und Klassen können oft dasselbe Problem lösen. Beide verbinden Daten und Verhalten, auf unterschiedliche Weise.

KriteriumClosureKlasse
Anzahl OperationenEine, vielleicht zweiMehrere zusammenhängende
ZustandKlein, einfachKomplex, mehrere Felder
Vererbung nötigNeinMöglich
BoilerplateSehr wenigMehr
Lesbarkeit für EinsteigerAnspruchsvollerVertrauter
Serialisierung (pickle)SchwierigEinfach

Mein Daumenwert: Eine zentrale Operation → Closure. Mehrere zusammenhängende Operationen → Klasse. Sobald du dich dabei erwischst, vier oder fünf Closures aus einer Factory zurückzugeben, hat sich heimlich eine Klasse versteckt. Mach sie sichtbar.

8. Closures und Decorators

Praktisch jeder Decorator ist eine Closure. Wer Decorators verstanden hat, hat Closures als Bonus mitgenommen, und umgekehrt.

import time
from functools import wraps

def timer(funktion):
    @wraps(funktion)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        ergebnis = funktion(*args, **kwargs)
        ende = time.perf_counter()
        print(f'{funktion.__name__}: {ende - start:.4f}s')
        return ergebnis
    return wrapper

timer ist die äußere Funktion, wrapper die innere, funktion die freie Variable, die wrapper einfängt. Genau das Muster aus den Kapiteln davor, nur mit @-Syntax garniert.

Wenn ein Decorator selbst Argumente bekommt, hast du drei verschachtelte Funktionen und damit zwei verschachtelte Closures. So funktionieren lru_cache(maxsize=128), retry(times=3), pytest.fixture(scope='module'): alle nach demselben Schema.

Wenn du tiefer in Decorators einsteigen willst, hab ich dazu einen eigenen Post geschrieben.

9. Komplett-Beispiel: Rate-Limiter

Zum Abschluss etwas, das sich in echtem Code lohnt: ein Rate-Limiter, der dafür sorgt, dass eine Funktion nicht öfter als n-mal pro Zeitintervall aufgerufen wird. Genau das, was du für API-Aufrufe, Login-Versuche oder teure Operationen brauchst.

import time
from collections import deque
from functools import wraps


def rate_limit(max_calls, pro_sekunden):
    """Decorator-Factory: erlaubt max_calls Aufrufe
    pro pro_sekunden Sekunden."""

    def decorator(funktion):
        # Verlauf: Zeitstempel der letzten Aufrufe
        verlauf = deque()

        @wraps(funktion)
        def wrapper(*args, **kwargs):
            jetzt = time.monotonic()

            # alte Einträge ausserhalb des Fensters entfernen
            while verlauf and jetzt - verlauf[0] > pro_sekunden:
                verlauf.popleft()

            if len(verlauf) >= max_calls:
                wartezeit = pro_sekunden - (jetzt - verlauf[0])
                raise RuntimeError(
                    f'Rate limit erreicht. Warte {wartezeit:.2f}s.'
                )

            verlauf.append(jetzt)
            return funktion(*args, **kwargs)

        return wrapper
    return decorator


@rate_limit(max_calls=3, pro_sekunden=1.0)
def api_call(endpoint):
    print(f'Anfrage an {endpoint}')


for i in range(5):
    try:
        api_call(f'/users/{i}')
    except RuntimeError as e:
        print(f'Fehler: {e}')
    time.sleep(0.2)

Drei Dinge sind hier interessant:

  • Drei verschachtelte Funktionen: rate_limit ist die Decorator-Factory mit den Konfigurationswerten, decorator der eigentliche Decorator, wrapper ersetzt die Originalfunktion.
  • Zustand pro dekorierter Funktion: verlauf lebt in der Closure, gehört zur jeweils dekorierten Funktion, und zwei verschiedene dekorierte Funktionen haben zwei getrennte Verläufe.
  • Mutation ohne nonlocal: Wir verändern verlauf mit popleft und append, weisen es aber nie neu zu. Den Container zu mutieren ist keine Zuweisung, also kein nonlocal nötig.

Das ist Closures in Reinform: Konfiguration, Zustand und Verhalten in einer kompakten Einheit, die du beliebig oft wiederverwendest.

Zusammenfassung

  • Definition: Eine Closure ist eine innere Funktion, die Variablen aus ihrem äußeren Scope einfängt und behält.
  • Voraussetzungen: Geschachtelte Funktion + Zugriff auf äußere Variable + Weitergabe nach außen.
  • Mechanismus: Eingefangene Variablen leben in Cell-Objekten, sichtbar über __closure__.
  • Mutation: nonlocal lässt dich eingefangene Variablen verändern, nicht nur lesen.
  • Late Binding: Closures lesen Werte beim Aufruf, nicht beim Definieren. Vorsicht in Schleifen.
  • Wann Closure, wann Klasse: Eine Operation und kleiner Zustand → Closure. Mehrere zusammenhängende Operationen → Klasse.

Wenn du das nächste Mal in fremdem Code eine Funktion siehst, die eine Funktion zurückgibt, weißt du, was läuft. Und warum.

Ähnliche Posts

Alle anzeigen
Thumbnail mit Text "Dekoratoren einfach erklärt"
App-Entwicklung pythondekoratoren

Python-Dekoratoren einfach erklärt

Dekoratoren gehören zu den beliebtesten Sprachfeatures der Programmiersprache Python. Lerne, eigene Dekoratorenzu erstellen, um Boilerplate-Code zu reduzieren.

4 min
Python-Community

Werde Teil der Python-Community

Tausch dich mit anderen Python-Lernenden aus, stell deine Fragen im Forum und schau dir die besten Lernvideos rund um Python an — kostenlos auf Skool.