Python Closures
Lerne, wie Python Closures funktionieren: verschachtelte Funktionen, erfasste Variablen, nonlocal, häufige Fallstricke und praktische Anwendungsfälle mit ausführbaren Beispielen.
Eine Closure ist eine verschachtelte Funktion, die sich die Variablen des umschließenden Scopes merkt, in dem sie definiert wurde – selbst nachdem die äußere Funktion bereits zurückgekehrt ist. Closures ermöglichen es, privaten Zustand an eine Funktion zu knüpfen, ohne eine Klasse zu verwenden – und machen sie zu einem der elegantesten Werkzeuge in Python für den Aufbau von Callbacks, Factories und zustandsbehafteten Hilfsfunktionen.
Diese Seite erklärt, wie Closures funktionieren, welche drei Bedingungen sie erfordern, häufige Fallstricke und praktische Anwendungsfälle.
Was ist eine Closure?
Wenn Python eine Funktion ausführt, erzeugt sie einen lokalen Scope, der verschwindet, sobald die Funktion zurückkehrt. Normalerweise ist jede dort definierte Variable dann weg. Eine Closure bildet die Ausnahme: Wenn eine innere Funktion auf eine Variable einer äußeren Funktion verweist, hält Python diese Variable in einem speziellen Cell-Objekt am Leben, und die innere Funktion trägt einen Verweis auf diese Cells mit sich – wohin auch immer sie weitergegeben wird.
Die einfachste Closure ist eine Funktions-Factory – eine Funktion, die eine andere Funktion erstellt und zurückgibt:
def make_multiplier(factor):
def multiply(n):
return n * factor # 'factor' is captured from the enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20Jeder Aufruf von make_multiplier erzeugt eine neue Closure mit einer eigenen unabhängigen Kopie von factor. double und triple sind völlig unabhängig voneinander, obwohl sie durch dieselbe Funktion erstellt wurden.
Drei Bedingungen für eine Closure
Eine Funktion ist eine Closure, wenn alle drei der folgenden Bedingungen zutreffen:
- Es gibt eine verschachtelte Funktion – eine Funktion, die innerhalb einer anderen Funktion definiert ist.
- Die verschachtelte Funktion referenziert eine Variable aus dem umschließenden Scope – diese Variable wird als freie Variable bezeichnet.
- Die umschließende Funktion gibt die verschachtelte Funktion zurück (oder übergibt sie an eine andere Stelle).
def outer():
message = 'Hello from outer' # free variable
def inner():
print(message) # inner references it
return inner # outer returns inner
greet = outer()
greet() # Hello from outerNachdem outer() zurückgekehrt ist, ist ihr lokaler Frame verschwunden – aber message überlebt innerhalb von greet.__closure__.
Eine Closure inspizieren
Python macht Closure-Cells über das Attribut __closure__ zugänglich:
def make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
print(add5(3)) # 8
print(add5.__closure__) # (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents) # 5__closure__ ist ein Tupel aus Cell-Objekten – eines pro erfasster Variable. Wenn eine Funktion keine Closure ist, ist __closure__ gleich None.
Erfasste Variablen mit nonlocal ändern
Standardmäßig kann man eine erfasste Variable lesen, aber nicht neu binden. Der Versuch, ihr einen Wert zuzuweisen, erstellt stattdessen eine neue lokale Variable, was in der Regel nicht das Gewünschte ist. Verwende das Schlüsselwort nonlocal, um Python mitzuteilen, dass die Variable des umschließenden Scopes gemeint ist:
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count # rebind the enclosing 'count', not a new local
count += step
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter(5)) # 7
counter2 = make_counter(10)
print(counter2()) # 11
print(counter()) # 8 — counter is unaffectedJeder Aufruf von make_counter erzeugt eine unabhängige count-Cell. counter und counter2 teilen keinen gemeinsamen Zustand.
Für einen tieferen Einblick in die Frage, wie Python entscheidet, zu welchem Scope eine Variable gehört, siehe Python Scope.
Häufiger Fallstrick: Closures in Schleifen
Ein klassischer Fehler ist das Erstellen von Closures innerhalb einer Schleife in der Erwartung, dass jede den aktuellen Wert der Schleifenvariable erfasst:
# Wrong — all functions capture the same 'i' cell
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] — not [0, 1, 2]Alle drei Lambdas teilen sich eine Cell, die die Schleifenvariable i enthält. Wenn sie aufgerufen werden, hat i bereits seinen Endwert 2 erreicht.
Lösung 1: Standardargument (erfasst nach Wert)
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # default arg is evaluated immediately
print([f() for f in funcs]) # [0, 1, 2]Lösung 2: Factory-Funktion
def make_func(i):
def f():
return i
return f
funcs = [make_func(i) for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]Die Factory-Funktion erzeugt für jede Iteration einen neuen Scope – und damit eine neue Cell. Dies ist der explizitere und besser lesbare Ansatz.
Praktische Anwendungsfälle
Partielle Anwendung
Closures sind eine leichtgewichtige Alternative zu functools.partial, wenn man eine vorkonfigurierte Version einer Funktion benötigt:
def make_power(exponent):
def power(base):
return base ** exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(4)) # 16
print(cube(3)) # 27Einfaches Memoization
Eine Closure kann ein Cache-Dictionary halten, das zwischen Aufrufen erhalten bleibt:
def make_memoized(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@make_memoized
def slow_square(n):
return n * n
print(slow_square(4)) # 16
print(slow_square(4)) # 16 (served from cache)
print(slow_square(7)) # 49Dieses Muster zeigt genau, wie Python Decorators im Hintergrund funktionieren – ein Decorator ist einfach eine Closure, die eine andere Funktion umhüllt.
Callback-Konfiguration
Closures sind praktisch für den Aufbau von Callbacks, die ein wenig Kontext eingebettet benötigen:
def make_logger(prefix):
def log(message):
print(f'[{prefix}] {message}')
return log
info = make_logger('INFO')
error = make_logger('ERROR')
info('Server started') # [INFO] Server started
error('Disk full') # [ERROR] Disk fullClosures vs. Klassen
Eine Closure und eine Klasse mit einer einzelnen Methode lösen oft dasselbe Problem. Die Wahl hängt von der Komplexität ab:
| Situation | Bevorzuge |
|---|---|
| Ein Zustandselement, ein Verhalten | Closure |
| Mehrere Methoden oder öffentliche Attribute | Klasse |
| Muss serialisiert werden (z. B. pickle) | Klasse |
| Übergabe eines Callbacks an eine andere Funktion | Closure |
# Class approach
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self, step=1):
self.count += step
return self.count
# Closure approach
def make_counter(start=0):
count = start
def increment(step=1):
nonlocal count
count += step
return count
return incrementBeide erzeugen identisches Verhalten. Die Closure ist kürzer; die Klasse ist leichter auffindbar und erweiterbar.
Fazit
Closures ermöglichen es einer verschachtelten Funktion, ihren eigenen privaten Zustand zu tragen, indem sie sich die Variablen aus dem Scope merkt, in dem sie definiert wurde. Die wichtigsten Punkte sind:
- Eine Closure erfordert eine verschachtelte Funktion, eine freie Variable und die Rückgabe oder Weitergabe der inneren Funktion.
- Verwende
nonlocal, wenn du eine erfasste Variable neu binden (nicht nur lesen) möchtest. - Vermeide den Schleifen-Fallstrick: Verwende eine Factory-Funktion oder ein Standardargument, um den Wert bei jeder Iteration festzuhalten.
- Closures sind die Grundlage von Decorators und stehen in engem Zusammenhang mit Pythons Scope-Regeln.