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:
- Eine innere Funktion, die in einer äußeren Funktion definiert ist.
- Eine freie Variable: die innere Funktion greift auf einen Namen zu, der weder Parameter noch lokal ist, sondern aus dem äußeren Scope stammt.
- 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üsselwort | Verweist auf | Verfügbar in |
|---|---|---|
global | Modul-Ebene | Allen Funktionen |
nonlocal | Umschließender Funktion | Geschachtelten Funktionen |
Faustregel: Wenn du in einer inneren Funktion einen
UnboundLocalErrorbekommst und der Name eigentlich aus dem äußeren Scope kommt, hast dunonlocalvergessen.
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.
| Kriterium | Closure | Klasse |
|---|---|---|
| Anzahl Operationen | Eine, vielleicht zwei | Mehrere zusammenhängende |
| Zustand | Klein, einfach | Komplex, mehrere Felder |
| Vererbung nötig | Nein | Möglich |
| Boilerplate | Sehr wenig | Mehr |
| Lesbarkeit für Einsteiger | Anspruchsvoller | Vertrauter |
| Serialisierung (pickle) | Schwierig | Einfach |
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_limitist die Decorator-Factory mit den Konfigurationswerten,decoratorder eigentliche Decorator,wrapperersetzt die Originalfunktion. - Zustand pro dekorierter Funktion:
verlauflebt in der Closure, gehört zur jeweils dekorierten Funktion, und zwei verschiedene dekorierte Funktionen haben zwei getrennte Verläufe. - Mutation ohne
nonlocal: Wir verändernverlaufmitpopleftundappend, weisen es aber nie neu zu. Den Container zu mutieren ist keine Zuweisung, also keinnonlocalnö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:
nonlocallä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
Python-Dekoratoren einfach erklärt
Dekoratoren gehören zu den beliebtesten Sprachfeatures der Programmiersprache Python. Lerne, eigene Dekoratorenzu erstellen, um Boilerplate-Code zu reduzieren.
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.