Python: Was macht das Schlüsselwort yield?

Eine Funktion die yield enthält verhält sich völlig anders als eine normale Funktion. Sie gibt keinen Wert zurück, und ihr Rumpf läuft zunächst überhaupt nicht.

Die kurze Antwort

Eine Funktion die yield enthält liefert einen Generator zurück. Der Rumpf der Funktion läuft erst dann los, wenn du den Generator zum ersten Mal nach einem Wert fragst.

def zaehler():
    print("...der Rumpf laeuft erst jetzt los")
    yield 1
    yield 2

g = zaehler()          # es wird nichts ausgegeben
print(next(g))         # jetzt startet der Rumpf, gibt aus und liefert 1

Ausgeführt:

user@pc:~$ python zaehler.py
calling counter() gave : generator
nothing printed yet -- now the first next():
...body starts running only now
next(g) -> 1
next(g) -> 2
next(g) -> StopIteration

Das ist die ganze Idee. yield gibt einen Wert an den Aufrufer zurück und friert die Funktion an genau dieser Stelle ein. Das nächste next() taut sie wieder auf und macht in der Zeile nach dem yield weiter.

Danach ist der Generator aufgebraucht

Das ist der Punkt, der einen schon mal einen Nachmittag kostet:

first  list(g) : [1, 2]
second list(g) : []

Der zweite Durchlauf ist leer. Kein Fehler, keine Warnung — einfach nichts. Ein Generator läuft genau einmal vorwärts und das war es. Wenn du die Werte zweimal brauchst, machst du entweder eine Liste daraus oder rufst die Generatorfunktion noch einmal auf und bekommst einen frischen Generator.

Eine Liste kannst du dagegen beliebig oft durchlaufen:

a list by contrast, twice : [0, 1, 4, 9, 16] / [0, 1, 4, 9, 16]

Woher die Speicherersparnis kommt

Üblicherweise heißt es, Generatoren sparen Speicher. Was das wert ist, gemessen mit tracemalloc:

list of 1,000,000 squares :    38.57 MiB
the same as a generator   :     0.00 MiB
sys.getsizeof(generator)  : 208 bytes, whatever n is

Die 208 Byte sind das Generator-Objekt selbst, und dieser Wert wächst nicht mit n. Ob du gleich zehn Werte erzeugst oder zehn Milliarden — das Objekt kostet dasselbe, weil es nur einen pausierten Stack Frame hält und sonst nichts.

Derselbe Vergleich für bescheidene 1000 Elemente: Die List Comprehension belegt 8856 Byte, der Generatorausdruck 208.

Beachte dabei, dass sich das nur auszahlt, wenn du die Werte einzeln verarbeitest. In dem Moment, in dem du list(gen_squares(1_000_000)) schreibst, bist du wieder bei 38,57 MiB — nur eben über einen Umweg.

Die Verzögerung ist echt

Das sollte man einmal gesehen haben, statt es zu glauben:

next() one at a time:
    computing 0
    got 0
    (nothing more computed until we ask)

Es wurde nur der erste Wert berechnet. Das macht Generatoren für große Dateien brauchbar, oder für Folgen die gar kein Ende haben. Und es ist der Grund, warum ein Generator auch dann schneller sein kann, wenn Speicher gar keine Rolle spielt: Brichst du die Schleife nach zehn Elementen ab, wurden auch nur zehn Elemente berechnet.

yield merkt sich, wo es war

Alle lokalen Variablen der Funktion überleben zwischen den Aufrufen, weil der Frame pausiert und nicht verworfen wird:

def laufende_summe():
    summe = 0
    while True:
        wert = yield summe
        if wert is None:
            return
        summe += wert
priming with next()      : 0
r.send(10)               : 10
r.send(5)                : 15
r.send(1)                : 16

send() schickt einen Wert in den Generator hinein, wo er zum Ergebnis des yield-Ausdrucks wird. Beachte das Vorbereiten: Ein frischer Generator hat sein erstes yield noch nicht erreicht, deshalb musst du ihn vor dem ersten send() einmal mit next() anstoßen.

Auf diesem Mechanismus wurde async/await ursprünglich aufgebaut. Wer ihn hier einmal nachvollzieht, bekommt ein gutes Gefühl dafür, wie Coroutinen darunter funktionieren.

Der Fehler, den man kennen sollte

Angenommen du willst aus einer Funktion drei Werte herausgeben. Das hier tut nicht, wonach es aussieht:

def mit_yield():
    yield [1, 2, 3]
list(with_yield())  : [[1, 2, 3]]   <- a list inside a list

yield irgendeineListe liefert die Liste als ein Element. Um ihre Elemente einzeln herauszugeben brauchst du yield from, eingeführt mit PEP 380:

def mit_yield_from():
    yield from [1, 2, 3]
list(with_yield_from()) : [1, 2, 3]

yield from delegiert außerdem an einen anderen Generator und erspart dir die Schleife for element in innen(): yield element:

manual loop  : ['a', 'b']
yield from   : ['a', 'b', 'c', 'd']

Was ein Generator nicht kann

Weil keine Folge im Speicher liegt, gibt es auch nichts zu zählen oder zu indizieren:

len(g)   -> TypeError: object of type 'generator' has no len()
g[0]     -> TypeError: 'generator' object is not subscriptable

Brauchst du eines von beidem, brauchst du eine Liste — und dann solltest du dir die Frage stellen, was dir der Generator an dieser Stelle überhaupt bringt. Für einen Ausschnitt ohne die ganze Folge zu materialisieren gibt es itertools.islice.

Die kurze Schreibweise: Generatorausdrücke

Für die einfachen Fälle brauchst du gar keine Funktion. Tausche die eckigen Klammern einer List Comprehension gegen runde:

sum(x*x for x in range(5))  : 30
type of (x*x for x in ...)  : generator

Da es das einzige Argument ist, übernehmen die Klammern des Aufrufs die Doppelrolle und ein zweites Klammernpaar ist nicht nötig.

Denk daran, dass hier dieselbe Regel gilt: Ein Generatorausdruck, den du einer Variablen zuweist, ist nach einem Durchlauf genauso aufgebraucht wie alles andere.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).