Funktionen, Scope (LEGB), Closures, Dekoratoren, Default-Falle

Track Python · PCAP-Fundament · ca. 55 Min.

Worum es geht

Funktionen kennst du aus JS/TS in- und auswendig. In Python sehen sie ähnlich aus, verhalten sich aber an drei Stellen anders: bei Default-Werten (default arguments), beim Scope (Namensauflösung) und bei Closures in Schleifen. Genau diese Stellen sind Gökays bekannte Lücken und gleichzeitig Standardfallen in der PCAP (Closures und Lambdas gehören laut Glossar zu Block 5 “Sonstiges”, 9 Fragen, 22 %, Stand der Abfrage 1. Oktober 2026). Nach dieser Lektion kannst du vorhersagen, was solcher Code ausgibt, und du kannst einen Dekorator (decorator) selbst schreiben.

Ablauf: erst die Übersetzung JS/TS nach Python, dann fünf kleine Schritte mit lauffähigem Code, dann fünf Übungen.

Von JS/TS her gedacht

Thema JS/TS Python
Funktion definieren function f(a) {} oder (a) => a def f(a): oder lambda a: a
Default-Wert wird bei jedem Aufruf neu ausgewertet wird einmal beim Definieren ausgewertet
Rest-Parameter ...args *args (Tupel) und **kwargs (dict)
Variable lesen sucht nach außen sucht nach außen (LEGB)
Variable schreiben x = 1 schreibt auf die äußere Variable, wenn es keine lokale gibt x = 1 legt immer eine lokale Variable an
Block-Scope let und const gelten nur im Block gibt es nicht: if, for, while bilden keinen Scope
Closure in Schleife let i ergibt pro Durchlauf ein neues i, var i nicht verhält sich wie var: ein i für alle

Dieselbe Idee in beiden Sprachen, der Default-Wert:

// TypeScript: jeder Aufruf bekommt ein neues Array
function add(x: number, items: number[] = []) {
  items.push(x);
  return items;
}
add(1); // [1]
add(2); // [2]

Im Python-Lauf steht in der zweiten Zeile [1, 2], nicht [2]. Warum, erklärt Schritt 1.

Konzept in kleinen Schritten

Schritt 1: Parameter und die Default-Falle

Die Argumentregeln im Überblick (Quelle: Glossar):

  • Positionsargument (positional argument): Zuordnung über die Reihenfolge, f(1, 2).
  • Keyword-Argument (keyword argument): Zuordnung über den Namen, f(b=2, a=1). Positionsargumente stehen davor.
  • Default-Wert (default value): def f(a, b=10). Parameter mit Default stehen nach denen ohne.
  • *args sammelt überzählige Positionsargumente als tuple, **kwargs überzählige Keyword-Argumente als dict.
  • Nur-Keyword-Parameter (keyword-only): alles nach * oder *args muss per Name übergeben werden.
  • Ohne return liefert eine Funktion None. return 1, 2 liefert ein Tupel.

Nun zur Default-Falle. Der Default-Wert wird einmal ausgewertet, nämlich in dem Moment, in dem def ausgeführt wird. Das Objekt hängt danach an der Funktion. Ist es veränderbar (mutable), teilen sich alle Aufrufe dasselbe Objekt. Du kannst es sogar ansehen:

Der Standardweg heraus ist None als Platzhalter (sentinel):

Wichtig: Prüfe mit is None, nicht mit if not items. Eine leere Liste, die der Aufrufer selbst übergibt, ist auch “falsch” (falsy). Dann würde items = items or [] still eine neue Liste anlegen, und der Aufrufer sieht seine eigene Liste nie gefüllt.

Schritt 2: Scope und LEGB

Python sucht einen Namen in dieser Reihenfolge: Local (die Funktion selbst), Enclosing (umschließende Funktion), Global (Modul), Built-in (len, print, …).

Die zentrale Regel: Jede Zuweisung in einer Funktion macht den Namen in der ganzen Funktion lokal. Das gilt auch für +=. Darum knallt dieser Code, obwohl es ein globales zaehler gibt:

In JS würde zaehler += 1 die äußere Variable ändern. In Python brauchst du eine Deklaration:

  • global x: die Funktion schreibt auf die Modul-Variable.
  • nonlocal x: die innere Funktion schreibt auf die Variable der umschließenden Funktion (Schritt 3).

Und es gibt keinen Block-Scope. Nur Funktionen, Klassen und Module bilden einen Scope:

Schritt 3: Closures

Eine Closure ist eine innere Funktion, die Variablen der äußeren Funktion behält, auch nachdem die äußere beendet ist. Das ist wie in JS. Der Unterschied: Zum Schreiben braucht es nonlocal.

Lambda ist eine anonyme Funktion mit genau einem Ausdruck, ohne return und ohne Anweisungen: lambda x, y: x + y. Typisch als key: sorted(items, key=lambda s: -len(s)).

Schritt 4: Die Closure-Falle in Schleifen

Eine Closure merkt sich die Variable, nicht ihren Wert zum Erzeugungszeitpunkt. Wie var in altem JS:

Alle drei Lambdas lesen dasselbe i, und das steht am Ende bei 2. Zwei Abhilfen:

Beachte die Ironie: Abhilfe 1 nutzt genau die Eigenschaft aus Schritt 1 (Default wird beim Definieren ausgewertet), die dort die Falle war.

Schritt 5: Dekoratoren

Ein Dekorator (decorator) ist eine Funktion, die eine Funktion bekommt und eine neue zurückgibt. @deco über def f ist gleichbedeutend mit f = deco(f). In JS/TS kennst du das als Higher-Order-Function.

Der Aufruf hello() geht jetzt durch wrapper. Drei Details sind wichtig:

  • *args, **kwargs reicht alle Argumente unverändert durch.
  • return fn(...) reicht den Rückgabewert durch. Vergisst du return, liefert jede dekorierte Funktion None.
  • functools.wraps(fn) kopiert Name und Docstring auf den Wrapper:

Falle: die Prüfungs- und Praxisklassiker

  1. Veränderbarer Default (def f(x, l=[])): die Liste wird über alle Aufrufe geteilt. Gilt genauso für {} und set().
  2. x += 1 auf eine äußere Variable: UnboundLocalError, weil die Zuweisung x lokal macht.
  3. Closure in der Schleife: [lambda: i for i in range(3)] liefert dreimal 2.
  4. if not items statt if items is None: leere, vom Aufrufer übergebene Container werden überschrieben.
  5. Dekorator ohne return oder ohne functools.wraps: Ergebnis weg oder Name verloren.

Übungen

Übung 1: Default-Falle vorhersagen

Dieser Code steht fest, du musst nur die Ausgabe vorhersagen:

def sammle(x, korb=[]):
    korb.append(x)
    return korb

sammle("a")
sammle("b")
drittes = sammle("c")

Was steht in drittes? Trage die Liste ein.

Der Default korb=[] wird nur einmal erzeugt, beim def. Was liegt darin, wenn der dritte Aufruf kommt?

antwort = ["a", "b", "c"]
antwort

Übung 2: Die Falle reparieren

Diese Funktion soll bei jedem Aufruf ohne eigene Liste mit einer frischen Liste starten. Schreibe sie so um, dass sie sich korrekt verhält. Wenn der Aufrufer eine Liste übergibt (auch eine leere), muss genau diese Liste gefüllt und zurückgegeben werden.

Schau noch einmal auf den Default-Platzhalter aus Schritt 1. Und achte auf den Fall, dass der Aufrufer eine leere Liste übergibt: Welche Prüfung unterscheidet “nichts übergeben” von “leer übergeben”?

def tag_hinzufuegen(tag, tags=None):
    if tags is None:
        tags = []
    tags.append(tag)
    return tags

tag_hinzufuegen

Übung 3: Scope vorhersagen

Zwei Snippets, zwei Antworten. Der Code steht fest:

# Snippet A
zaehler = 0
def erhoehe():
    zaehler += 1
    return zaehler
# Was passiert bei erhoehe()? Gesucht: der Name der Exception als String.

# Snippet B
for i in range(3):
    pass
# Gesucht: der Wert von i nach der Schleife.

Trage ein Tupel (name_der_exception, wert_von_i) ein.

A: Eine Zuweisung (auch +=) macht den Namen in der ganzen Funktion lokal. Es gibt aber einen speziellen Exception-Namen dafür. B: Gibt es in Python einen Block-Scope für for?

antwort = ("UnboundLocalError", 2)
antwort

Übung 4: Closure in der Schleife

Die Funktion soll eine Liste von Funktionen liefern. Die k-te Funktion addiert zahlen[k] zu ihrem Argument. So wie sie jetzt dasteht, addieren alle dieselbe Zahl. Repariere sie.

Alle Lambdas teilen sich ein z, und das steht am Ende beim letzten Wert. Du musst den Wert zum Zeitpunkt des Erzeugens festhalten. Schritt 4 zeigt zwei Wege.

def mache_addierer(zahlen):
    return [lambda x, z=z: x + z for z in zahlen]

mache_addierer

Übung 5: Einen Dekorator schreiben

Schreibe den Dekorator protokolliert. Er hängt bei jedem Aufruf den Namen der dekorierten Funktion an die Liste protokoll an (die Liste existiert schon). Er muss alle Argumente (positionell und per Keyword) durchreichen, den Rückgabewert liefern und den Namen der Funktion erhalten.

Der Wrapper hat zwei Aufgaben: etwas notieren und dann die echte Funktion aufrufen, mit allen Argumenten und mit durchgereichtem Ergebnis. Der Name steht in fn.__name__. Und was muss protokolliert am Ende zurückgeben? Schau auf Schritt 5.

import functools

def protokolliert(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        protokoll.append(fn.__name__)
        return fn(*args, **kwargs)
    return wrapper

protokolliert

Merksatz und Prüfstein

Merksatz: Der Default-Wert entsteht einmal beim def, eine Zuweisung macht den Namen lokal, und eine Closure merkt sich die Variable, nicht ihren Wert.

Prüfstein (offene Frage): Erkläre in eigenen Worten, warum lambda i=i: i in einer Schleife funktioniert, und welche Eigenschaft von Default-Werten dafür verantwortlich ist, die in Schritt 1 noch die Falle war.

Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitte “Funktionen, Scope, Closures und Dekoratoren” (Parameter und Argumente, “Die Default-Falle”, “Scope (LEGB)”, “Lambda”, “Closure”, “Dekoratoren”) sowie Prüfungsblöcke (PCAP-31-03). Hinweis: functools.wraps und __defaults__ stehen nicht in der Quelle, wurden aber mit Python 3.13 ausgeführt und geprüft (bitte prüfen, ob sie in der PCAP geprüft werden).