Closures in Python

Auch Python kennt Closures. Für alle die dieses Konstrukt nicht kennen, versucht dieser Blogbeitrag etwas Licht ins Dunkel zu bringen.

Um Closures zu verstehen, muss man sich zunächst mit verschachtelten Funktionen beschäftigen. Folgender Code soll dies demonstrieren:

def function_1():
    def function_2_():
        pass

In diesem einfachen Beispiel ist function_2() Bestandteil der sie umschließenden Funktion function_1(). Bei function_2() handelt es sich um eine verschachtelte (auch: innere) Funktion (nested function).

Des Weiteren ist das Verständnis der Gültigkeit von Variablen von besonderer Bedeutung. Sobald eine Variable in einer Funktion definiert wird, wird sie an den lokalen Namensraum der Funktion gebunden (Man spricht hierbei auch vom Gültigkeitsbereich der Variable.) . Nachfolgendes Beispiel soll dies veranschaulichen:

number = 100


def my_function():
    number = 200


if __name__ == '__main__':
    my_function()
    print(number)  # -> 100

Die Ausführung der print-Funktion führt zur Ausgabe von 100, nicht 200. Denn die in function_1 definierte Variable number ist nur an den lokalen Namensraum der Funktion gebunden; sie ist nur hier gültig. Eine Änderung in dieser Funktion hat daher keinen Einfluß auf eine (gleichnamige) Variable im globalen Namensraum. Entwicklungsumgebungen — wie beispielsweise PyCharm — zeigen in so einem Fall übrigens einen entsprechenden Hinweis an.

Ändern ließe sich dies mit Schlüsselwort global:

number = 100


def my_function():
    global number
    number = 200


if __name__ == '__main__':
    my_function()
    print(number)  # -> 200

Jetzt wird 200 ausgegeben, also jener Wert, der innerhalb von my_function() der Variablen number zugewiesen wurde.

Mit diesem Wissen können wir nun zu den verschachtelten Funktionen zurückkehren. Betrachten wir zunächst folgendes Beispiel:

def function_1(num1):

    def function_2():
        print(num1 * 2)

    function_2()  # Aufruf der Funktion


if __name__ == '__main__':
    function_1(10)  # -> 20

Innerhalb der äußeren Funktion function_1() befindet sich die verschachtelte Funktion function_2(). Wie zu erwarten, wird nach dem Aufruf von function_1(10) der Wert 20 ausgegeben.

Die Funktion function_2() wird innerhalb von function_1() aufgerufen. Python ermöglicht es aber auch, eine (verschachtelte) Funktion zurückzugeben, wie beim folgenden Beispiel zu sehen ist:

def function_1(num1):

    def function_2():
        print(num1 * 2)

    return function_2  # Rückgabe der Funktion


if __name__ == '__main__':
    result = function_1(10)  # -> 20
    result()

Im Ergebnis ist kein Unterschied zu sehen. Auch hier wird der Wert 20 zurückgegeben.

Jetzt wird dieses Beispiel um eine Variabel num2 ergänzt, die im Namensraum der Funktion function_1 definiert wird. Diese Variable num2 wird dann in der inneren Funktion anstelle des Wertes „2“ als Operand genutzt:

def function_1(num1):

    num2 = 2

    def function_2():
        print(num1 * num2)

    return function_2


if __name__ == '__main__':
    result = function_1(10)
    result() # -> 20

Auch in diesem Fall lautet das Ergebnis „20“. Das Interessante ist nun aber, dass die innere Funktion function_2() die Variable num2 verwenden kann, obwohl sie zur äußeren Funktion function_1 gehört. Hierbei spricht man von einem Closure:

    num2 = 2

    def function_2():
        print(num1 * num2)

Die innere Funktion ist in der Lage, eine äußere, zu einem anderen Namensraum gehörende Variable (hier: num2) „einzufangen“ und dementsprechend zu verwenden. Die Variable num2 steht also in zwei Gültigkeitsbereichen zur Verfügung, nämlich in function_1() und in function2_().

Im gezeigten Beispiel wird mit

result = function_1(10)

der Rückgabewert der Variablen result zugewiesen. In der nächsten Zeilen kann result() dann aufgerufen werden, da es sich hierbei um eine Funktion handelt.

Zum Zeitpunkt dieses Aufrufs ist function_1() bereits beendet. Mit anderen Worten: Der Gültigkeitsbereich von function_1() existiert nicht mehr (und damit sollten eigentlich auch die Variablen nicht mehr existieren). Das Besondere ist nun aber, dass dennoch result() ausgeführt werden kann und ein Ergebnis liefert. Denn wie oben hinsichtlich der Variable num2 gezeigt, verhält es sich auch hier so, dass der an function_1() übergebene Wert (hier: 10) beim Aufruf von result() weiterhin verwendet wird, obwohl der Gültigkeitsbereich von function_1() nicht mehr existiert.

Dass es sich bei result() tatsächlich um ein Closure handelt kann übrigens überprüft werden. Und zwar anhand des Attributs __closure__, das im Falle des Vorhandenseins eines Closures ein Tuple mit cell-Objekten zurückgibt.

Die Ausführung von

print(result.__closure__)

führt bei mir zu folgender Ausgabe:

(<cell at 0x1101b4700: int object at 0x11008c210>, <cell at 0x1102de890: int object at 0x11008c110>)

Zurückgeben werden die Speicheradressen der „eingefangenen“ Werte (von num1 und num2).

Anders verhält es sich bei function_1(). Die Ausführung von

print(function_1.__closure__)

gibt None zurück. Es handelt sich also nicht um ein Closure.

Bleibt zum Schluß noch die Frage: Wann spielen Closures eine Rolle? Sie kommen insbesondere bei einem Konstrukt zum Tragen, dass als Decorators bezeichnet wird. Vermutlich werdet Ihr ihnen schon begegnet sein. Sie sind an dem vorangestellten @-Zeichen erkennbar.