R

ReflexKR

Reflex-Dokumentation

Der Etiketten-Flughafen: Pythons Objektmodell

·

Kursüberblick

1

Das Etikett ist nicht der Koffer: Namen und Objekte

2

Versiegelte Koffer: veränderlich und unveränderlich

3

Koffer im Koffer: Container und Kopien

4

Wenn ein Koffer übergeben wird: Funktionen und Argumente

5

Zonen und Ausweise: Gültigkeitsbereiche

6

Handgepäck: Closures

7

Der Koffer-Bauplan: Klassen und Instanzen

8

Flughafen-Standardverfahren: Dunder-Methoden

9

Der Bauplan hinter dem Bauplan: Vererbung und MRO

10

Die Kontrolle: das Geheimnis des Attributzugriffs

11

Herrenlose Koffer: Speicher und GC

12

Schließfachnummern: Hash, Gleichheit und Gesamtbild

Der Etiketten-Flughafen: Pythons Objektmodell
Lektion 6

Zeit 4:10–5:00

Handgepäck: Closures

Verlustmeldung

LOST-006

Drei Fluggäste, alle am selben Gate

Gemeldet von

Seo, zuständig für die Boarding-Durchsagen

Was fehlt

Drei Funktionen für Gate-Hinweise. In einer Schleife wurden sie für A1, B7 und C3 erzeugt, doch alle drei schicken die Fluggäste zu C3.

Erledigt, wenn

Nachsehen, was die drei Funktionen mit sich tragen, und dafür sorgen, dass sich jede Funktion ihr eigenes Gate merkt.

Internationaler Flughafen Haneulseom · Gepäckzentrum

Kurz nach der Morgendurchsage stehen drei Fluggäste, die zu verschiedenen Gates müssten, alle vor C3. Seo sagt, es gebe doch drei Hinweisfunktionen, also müsse es auch drei verschiedene Hinweise geben.

Die Schichtleiterin hält ein Stück Handgepäck hoch. „Auch Funktionen tragen einen Koffer mit sich. Die Frage ist, ob darin ein Wert steckt oder ein Namensschild.“ Heute öffnen wir den kleinen Koffer, den Funktionen mit sich herumtragen: die Closure.

6-1. Funktionen, die Funktionen zurückgeben (7 min)

Wie in Lektion 1 gesehen, sind auch Funktionen Objekte. Man kann sie an Namen binden, in Listen stecken, an andere Funktionen übergeben und zurückgeben.

def make_tag(owner):
    def tag():
        return "owner: " + owner
    return tag

t = make_tag("kim")
print(t())
# owner: kim
print(t.__name__, t.__qualname__)
# tag make_tag.<locals>.tag
  • Bei jedem Aufruf von make_tag läuft def tag():, und es entsteht ein neues Funktionsobjekt.
  • In dem Moment, in dem t() aufgerufen wird, ist make_tag längst beendet. Nach den Regeln aus Lektion 5 hätte der lokale Name owner von make_tag mitverschwinden müssen.
  • Trotzdem erinnert sich t() an "kim". Wie hält die innere Funktion einen Namen einer Funktion fest, die schon beendet ist?

6-2. Das Handgepäck: Zellen und __closure__ (9 min)

Benutzt eine innere Funktion einen Namen der äußeren Funktion, legt Python diesen Namen in einen kleinen Koffer namens Zelle (cell). Der Rahmen der äußeren Funktion und die innere Funktion zeigen auf dieselbe Zelle. Endet die äußere Funktion und verschwindet ihr Rahmen, bleiben Zelle und Wert erhalten, weil die innere Funktion die Zelle festhält. Eine Funktion, die ein solches Bündel von Zellen mit sich trägt, ist eine Closure.

def make_tag(owner):
    def tag():
        return "owner: " + owner
    return tag

t = make_tag("kim")
print(t.__code__.co_freevars)
# ('owner',)
print(type(t.__closure__[0]).__name__)
# cell
print(t.__closure__[0].cell_contents)
# kim
print(make_tag.__code__.co_cellvars)
# ('owner',)
AttributBedeutung
inner.__code__.co_freevarsdie Namen, die sich die innere Funktion von außen borgt (freie Variablen)
inner.__closure__ein Tupel mit einer Zelle pro solchem Namen
cell.cell_contentsdas Objekt, auf das die Zelle gerade zeigt
outer.__code__.co_cellvarsdie Namen, die die äußere Funktion als Zelle angelegt hat

Solange make_tag läuft, zeigt die Etikettentafel owner im Rahmen als den Wert, der in der Zelle steckt. Achte darauf, ob die Zeile owner von function tag auch nach dem Verschwinden des Rahmens noch über cell an 'kim' festhält.

Etikettentafel

Schritt 1 / 0

Als Nächstes läuft: 0

Labor wird vorbereitet…

Gerade geändert

Versiegelter Koffer (unveränderlich)

Wenn make_tag endet, verschwindet sein Rahmen, aber die cell, die die Funktion tag trägt, hält 'kim' weiter fest.
  • Ins Handgepäck kommen nur die Namen, die die innere Funktion tatsächlich benutzt. Die übrigen lokalen Namen der äußeren Funktion werden aufgeräumt, wenn sie endet.
  • Umgekehrt bleibt alles, was hineingekommen ist, so lange erhalten, wie die Funktion lebt. Benutzt die innere Funktion auch nur einmal eine große Liste, bleibt diese Liste mit erhalten.
def make_tag(owner):
    notes = ["memo"] * 1000
    def tag():
        return owner
    return tag

print(make_tag("kim").__code__.co_freevars)
# ('owner',)

Pin sagt

→ Eine Closure trägt keine Kopie des Werts, sondern eine Zelle, also den Platz, an dem das Namensschild hängt. Hängt man den Namen draußen um, ändert sich auch der Wert, den die Closure sieht.

6-3. Späte Bindung: warum alle drei Fluggäste zu C3 gingen (10 min)

Das ist der Code aus der Meldung:

def make_guides(gates):
    guides = []
    for gate in gates:
        guides.append(lambda: gate)
    return guides

gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['C3', 'C3', 'C3']
print(gs[0].__closure__[0] is gs[2].__closure__[0])
# True
  • Schleifen erzeugen keinen Bereich (Lektion 5). Während der drei Durchläufe ist gate ein einziger Name in make_guides, und es gibt auch nur eine Zelle.
  • Alle drei lambda nehmen diese eine Zelle mit. Am Ende der Schleife steckt darin "C3".
  • Das gate im Rumpf des lambda schaut beim Aufruf in die Zelle. Beim Erzeugen wird der Wert nicht notiert. Das nennt man späte Bindung (late binding).

Mit einer List Comprehension ist es genauso:

fs = [lambda: i for i in range(3)]
print([f() for f in fs])
# [2, 2, 2]
print(fs[0].__closure__[0] is fs[1].__closure__[0])
# True

Auch ohne Schleife passiert dasselbe. Hängt man den Namen draußen um, nachdem die Funktion erzeugt wurde, ändert sich auch der Wert, den sie sieht.

def make():
    gate = "A1"
    def guide():
        return gate
    gate = "Z9"
    return guide

print(make()())
# Z9

Achte auf der Etikettentafel unten darauf, wie die Pfeile der drei Funktionen in einer einzigen Zelle zusammenlaufen. In der korrigierten Fassung hat jede Funktion ihr eigenes defaults-Tupel.

Etikettentafel

Schritt 1 / 0

Als Nächstes läuft: 0

Labor wird vorbereitet…

Gerade geändert

Versiegelter Koffer (unveränderlich)

Bei lambda: gate zeigen die gate aller drei Funktionen auf dieselbe cell. Mit gate=gate landet in jeder Funktion der Wert vom Zeitpunkt ihrer Erzeugung im Standardargument.

Die Lösung: Man legt „den Wert vom Moment der Erzeugung“ eigens in der Funktion ab.

fs = [lambda i=i: i for i in range(3)]
print([f() for f in fs])
# [0, 1, 2]

from functools import partial

def guide(gate):
    return "go to " + gate

guides = [partial(guide, gate) for gate in ["A1", "B7", "C3"]]
print([g() for g in guides])
# ['go to A1', 'go to B7', 'go to C3']
  • lambda i=i: i: Standardargumente werden einmal bei der Definition berechnet (Lektion 4). Das i dieses Moments landet in __defaults__ der Funktion.
  • functools.partial(guide, gate): erzeugt ein neues aufrufbares Objekt, das die Funktion mit dem Argument dieses Moments bündelt. Die Absicht ist klarer als beim Trick mit dem Standardargument.
  • Die Variante mit dem Standardargument hat einen Nachteil: Der Aufrufer kann den Wert überschreiben, etwa mit f(5).

6-4. Funktionen mit Zustand: ein Zähler mit nonlocal (7 min)

Zellen kann man nicht nur lesen. Mit nonlocal (Lektion 5) kann die innere Funktion das Etikett in der Zelle umhängen. Dann hat die Funktion einen Zustand, der von Aufruf zu Aufruf erhalten bleibt.

def counter():
    n = 0
    def tick():
        nonlocal n
        n += 1
        return n
    return tick

c = counter()
c()
c()
print(c(), c.__closure__[0].cell_contents)
# 3 3
c2 = counter()
print(c2(), c())
# 1 4
  • Jeder Aufruf von counter() legt eine neue Zelle an. c und c2 wissen nichts von der Zahl des anderen.
  • Ohne nonlocal n würde n += 1 den Namen n lokal machen, und es gäbe einen UnboundLocalError (dieselbe Ursache wie in Lektion 5).

Etikettentafel

Schritt 1 / 0

Als Nächstes läuft: 0

Labor wird vorbereitet…

Gerade geändert

Versiegelter Koffer (unveränderlich)

Bei jedem Aufruf von tick wechselt der Wert in der cell von 0 auf 1 und auf 2. Ganzzahlen sind unveränderlich, also ändert sich nicht die Zahl, sondern das Etikett in der Zelle wandert zu einer neuen Zahl.

Dasselbe geht auch mit einer Klasse. Dann steckt der Zustand in einem Attribut der Instanz (Lektion 7).

class Counter:
    def __init__(self):
        self.n = 0
    def tick(self):
        self.n += 1
        return self.n

k = Counter()
k.tick()
print(k.tick(), k.n)
# 2 2
ClosureKlasse
Wo der Zustand liegtin der Zelleim __dict__ der Instanz
Von außen ansehennur über __closure__direkt mit k.n
Passt gut, wennein Verhalten, wenig Zustandmehrere Verhalten, viel Zustand

6-5. Ein erster Blick auf Dekoratoren (7 min)

Eine Closure, die eine Funktion entgegennimmt und eine neue Funktion zurückgibt, heißt Dekorator. Die ursprüngliche Funktion steckt in der Zelle der inneren Funktion.

def log(func):
    def wrapper(*args, **kwargs):
        print("call", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@log
def board(seat):
    return "seat " + seat

print(board("12A"))
# call board
# seat 12A
print(board.__name__)
# wrapper
  • @log ist dasselbe wie board = log(board). Der Name board zeigt jetzt auf die Funktion wrapper, und das ursprüngliche board steckt in deren Zelle func.
  • Deshalb ergibt board.__name__ jetzt wrapper. Beim Debuggen und in der Dokumentation ist das lästig.

Eine einzige Zeile functools.wraps überträgt Name, Docstring und Ähnliches der ursprünglichen Funktion.

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("call", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@log
def board(seat):
    return "seat " + seat

print(board.__name__, board.__wrapped__.__name__)
# board board

Wie man Dekoratoren Argumente mitgibt, Dekoratoren als Klassen baut und mehr: Das vertiefen wir im Kurs über Generatoren und Dekoratoren.

Fall abgeschlossen (10 min)

Es genügt, jeder Funktion das Gate vom Zeitpunkt ihrer Erzeugung mitzugeben.

from functools import partial

def announce(gate):
    return "go to " + gate

def make_guides(gates):
    return [partial(announce, gate) for gate in gates]

gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['go to A1', 'go to B7', 'go to C3']

def make_guides_v2(gates):
    return [lambda gate=gate: "go to " + gate for gate in gates]

print([g() for g in make_guides_v2(["A1", "B7", "C3"])])
# ['go to A1', 'go to B7', 'go to C3']

Ab der nächsten Durchsage geht jeder der drei Fluggäste zu seinem eigenen Gate. Als Seo gegangen ist, blättert die Schichtleiterin durch die Aufzeichnungen von Band 13 und murmelt vor sich hin: „Auch die Sachen einer beendeten Funktion werden nicht entsorgt, wenn jemand sie im Handgepäck festhält. Vielleicht hält auch jemand diese zwei Koffer fest.“

Abschlussbericht — LOST-006. Die drei Hinweisfunktionen teilten sich eine Zelle der Schleifenvariable gate und lasen beim Aufruf den letzten Wert C3 (späte Bindung). Gelöst mit partial und Standardargumenten, die den Wert vom Zeitpunkt der Erzeugung festhalten.

Übungen

  1. Was kommt heraus, wenn du [lambda: i for i in range(3)] zu [lambda: i * 10 for i in range(3)] änderst? Prüfe mit __closure__, wie viele Zellen es gibt.
  2. Ändere counter() so, dass es zwei Funktionen zurückgibt: tick() und reset(). Prüfe mit is, ob beide dieselbe Zelle benutzen.
  3. Schreib einen Dekorator count_calls, der zählt, wie oft eine Funktion aufgerufen wurde. Kommt die Anzahl in ein Attribut von wrapper oder in eine Zelle? Probier beides aus.

Das Notizbuch der Schichtleiterin

  • Auch Funktionen sind Objekte. Man kann Funktionen erzeugen und zurückgeben.

  • Äußere Namen, die eine innere Funktion benutzt, kommen in Zellen, und die Funktion trägt diese Zellen in __closure__ mit sich.

  • Eine Closure trägt keinen Wert, sondern eine Zelle (den Platz des Namensschilds). Geöffnet wird sie beim Aufruf: späte Bindung.

  • In einer Schleife erzeugte Funktionen teilen sich eine Zelle. Lösung: lambda i=i: i, functools.partial.

  • Hängt nonlocal das Etikett in der Zelle um, wird die Funktion zu einer Funktion mit Zustand. Bei viel Zustand: eine Klasse.

  • Ein Dekorator ist eine Closure, die eine Funktion nimmt und eine Funktion zurückgibt. functools.wraps nicht vergessen.

← Zurück

Lektion 5 · Zonen und Ausweise: Gültigkeitsbereiche

Weiter →

Lektion 7 · Der Koffer-Bauplan: Klassen und Instanzen

Über neue Lektionen informiert werden

Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.

Erhoben: deine E-Mail-Adresse · Zweck: Hinweise auf neue Lektionen oder den Start dieses Kurses · Speicherdauer: bis du die Löschung verlangst. Keine andere Nutzung, keine Weitergabe. Antworte einfach auf eine Benachrichtigung, dann löschen wir sie sofort.

Fragen & Antworten

Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.

Wird als reiner Text angezeigt, ohne Links oder Formatierung.

R

ReflexKR

Python und Reflex zum Anfassen. Alle Beispiele wurden mit Reflex 0.10.0 ausgeführt und geprüft.

ReflexKR ist kostenlos und ohne Werbebanner. Wenn es dir geholfen hat, kannst du es mit einem Kaffee unterstützen. ☕ Spendier uns einen Kaffee

Hinweis zu Affiliate-Links
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex