0%

Iteration als Schnittstelle · Übung

Verzögerte Ausdrücke und wann eine Liste einfacher ist

Kapitel 3 führte den Generatorausdruck als Comprehension ohne eckige Klammern ein und versprach später eine Begründung. Jetzt ist es so weit.

[question.points for question in quiz]     # builds a list
(question.points for question in quiz)     # builds a generator object

Das zweite Verhalten heißt verzögerte Auswertung, auf Englisch lazy: Beim Erzeugen des Generators wird noch keiner der Punktwerte berechnet. Jeder Wert entsteht erst auf Anfrage. Das vollständige Ergebnis muss nie gleichzeitig gespeichert werden.

Die verzögerte Auswertung beobachten

Try it

Nichts entsteht, bevor etwas danach fragt. Vergleiche eine Listen-Comprehension, die alles durchläuft, bevor sie etwas zurückgibt:

Try it

Das ist der gesamte Unterschied. Alles andere folgt daraus.

Wann die Verzögerung wichtig ist

Früh aufhören. Brauchst du nur den ersten Treffer, erledigt eine Listen-Comprehension trotzdem die gesamte Arbeit:

Try it

Es prüfte 3, prüfte 9, fand den Treffer und hörte auf. Die Listenversion hätte alle vier geprüft.

Zu viel zum Speichern. Eine Million Zeilen in einer Liste sind eine Million Zeilen im Speicher. Verzögert und einzeln weitergereicht ist es eine Zeile.

Schritte verketten. Generatoren können Generatoren beliefern, ohne dass ein Schritt eine Sammlung aufbaut:

Try it

Jeder Generator benennt einen Schritt, ohne eine weitere Sammlung zu erzeugen. Die ursprüngliche Punkteliste bleibt bestehen, und list(doubled) sammelt das Endergebnis.

Stoppen, ohne ein zusätzliches Element zu nehmen

Nach zwei Ergebnissen aufzuhören sollte auch bedeuten, exakt zwei Eingabeelemente anzufordern. Eine for-Schleife fordert das nächste Element ihres Iterators vor dem Betreten des Schleifenrumpfs an. Diese Prüfung kommt daher zu spät:

for item in items:
    if len(collected) == 2:
        break
    collected.append(item)

Wenn die Bedingung zwei gesammelte Elemente sieht, hat die Schleife das dritte bereits angefordert. Prüfe unmittelbar nach der Annahme des Elements, das die Grenze erreicht:

def first_two(items):
    taken = 0
    for item in items:
        yield item
        taken = taken + 1
        if taken == 2:
            return

Ist die angeforderte Anzahl null oder kleiner, kehre zurück, bevor die Schleife überhaupt beginnt. Das ist exakt begrenzter Verbrauch: Die Funktion beendet sowohl ihre Ergebnisse als auch ihre Eingabeanforderungen an der zugesagten Grenze.

next(score for score in scores if score > 7) auf [3, 9, 5, 8]. Wie viele Punktestände prüft die Bedingung?

Wann eine Liste einfacher ist, und das ist oft

Verzögerte Auswertung hat einen Preis:

Nur ein Durchlauf. Der Fehler aus Lektion 1 lässt sich nun leicht versehentlich schreiben.

Keine Länge. len() auf einem Generator löst TypeError aus. Indexzugriffe ebenfalls.

Schwieriger zu untersuchen. Eine ausgegebene Liste zeigt ihre Werte. Ein ausgegebener Generator zeigt, dass er ein Generator ist.

Die Arbeit geschieht anderswo. Eine Ausnahme innerhalb eines Generators erscheint an der verbrauchenden for-Schleife. Diese kann weit vom scheinbar verantwortlichen Code entfernt sein.

Eine kurze praktische Regel:

GewünschtVerwende
Werte an sum, any, all, min, max oder next liefernEinen Generatorausdruck
Früh bei einer großen oder endlosen Quelle stoppenEinen Generator
Ein Ergebnis zurückgeben, das aufrufender Code indiziert, misst oder wiederverwendetEine Liste
Werte beim Debuggen betrachtenEine Liste
Eine Handvoll ElementeWas sich besser liest, meistens eine Liste

Bei zehn Fragen ist die Speicherersparnis belanglos. Die Lesbarkeit einer Liste ist mehr wert als die Eleganz verzögerter Auswertung.

Einen Generator aus einer Funktion zurückgeben

Gibt eine Funktion einen Generator zurück, sage das. Der aufrufende Code erhält einen einmal durchlaufbaren Wert ohne Länge:

def passing_records(records, mark):
    """Yield each record reaching the mark.

    Returns a generator: walk it once, or wrap it in list() to keep it.
    """

So benennst du den Kompromiss ehrlich. Für eine öffentliche Methode, die eine Sammlung zurückgibt, ist eine Liste der Standard. Die Ausnahme ist eine tatsächlich große Sammlung oder ein Aufrufer, der tatsächlich früh aufhört.

Aufgabe

Implementiere preview(items, count). Gib eine Liste mit höchstens count Elementen aus einer möglicherweise endlosen Quelle zurück.

Die Grenze ist exakt: Zwei Elemente zu nehmen muss das dritte in einem vorhandenen Iterator unangetastet lassen. Ist count null oder negativ, gib eine leere Liste zurück, ohne Eingaben anzufordern.

Die vorgegebenen Funktionen first_failing, total_points, high_scorers und describe zeigen die übrigen Entscheidungen zwischen verzögerten Ausdrücken und Listen aus der Lektion. Lies und führe sie aus, aber lass sie unverändert.