Kapitel 11 · Übung
Iteration als Schnittstelle
Iterable und Iterator
Du hast Hunderte for-Schleifen geschrieben. Hier siehst du, was eine davon tatsächlich tut.
for question in questions:
print(question.prompt)
Python durchläuft questions nicht anhand eines Index. Es bittet questions um einen Iterator und fragt diesen dann nach jeweils einem Element, bis er meldet, dass keines mehr übrig ist. Die Schleife ist ein Gespräch mit zwei Beteiligten. Wenn du sie unterscheiden kannst, verstehst du eine ganze Gruppe verwirrender Verhaltensweisen.
Zwei verschiedene Dinge
Ein Iterable, ein iterierbares Objekt, ist alles, was du in einer Schleife durchlaufen kannst: eine Liste, ein String, ein Dictionary, ein Set, eine Datei.
Ein Iterator übernimmt das Durchlaufen. Er hält die Position und weiß, was als Nächstes kommt.
Meist sind beide nicht dasselbe Objekt:
iter() bittet ein Iterable um einen Iterator. Eine Liste ist nicht ihr eigener Iterator. Sie gibt bei jeder Anfrage einen neuen heraus. Ein Iterator gibt dagegen sich selbst zurück und behält seine Position. Er ist deshalb ebenfalls iterierbar:
Das ist der Entwurf. Seine Folge siehst du sofort.

Ein Iterator wird verbraucht
Das erste list(walker) sammelte alle drei Elemente. Das zweite sammelte nichts. Der Iterator sprang nicht zurück, denn er hat keinen Rückweg: Er hält nur eine Position, und diese liegt jetzt hinter dem Ende.
Die Liste selbst ist unverändert:
Beide Aufrufe liefern drei Elemente, denn jeder list()-Aufruf hat questions um einen neuen Iterator ab dem Anfang gebeten.
Warum lässt sich dieselbe Liste zweimal durchlaufen, derselbe Iterator aber nicht?
Wo das Probleme macht
Das ist kein nebensächliches Detail. Es verursacht einen bestimmten Fehler, bei dem Daten scheinbar verschwinden:
Dieselben Zahlen, zwei verschiedene Antworten. Bei einer Liste erhält sum() einen Iterator und verbraucht ihn. Danach fordert list(scores) einen neuen von der Liste an: drei Elemente. Bei einem Iterator verbraucht sum() den einzigen vorhandenen, und list(scores) findet nichts mehr.
Die Funktion ist weniger falsch als unzureichend beschrieben: Sie durchläuft ihr Argument zweimal, ohne das zu sagen. Schreibst du eine Funktion, die ihre Eingabe mehrmals durchläuft, dokumentiere entweder die nötige wiederholt durchlaufbare Sammlung oder wandle die Eingabe zuerst in eine Liste um und benenne den Aufwand ausdrücklich.
Warum überhaupt diese Trennung
Es wäre einfacher, wenn eine Liste sich selbst durchliefe. Die Trennung bringt zwei Vorteile.
Mehrere Schleifen gleichzeitig. Zwei verschachtelte Schleifen über dieselbe Liste erhalten jeweils eine eigene Position. Das Ende der inneren beendet deshalb nicht die äußere.
Neun, nicht drei. Mit einer einzigen gemeinsamen Position könnte das nicht funktionieren.
Dinge, die zu groß zum Speichern sind. Ein Iterator muss nur wissen, wie er das nächste Element erzeugt. Nicht alle müssen gleichzeitig existieren. So kann ein Programm eine Datei, die größer als der Arbeitsspeicher ist, zeilenweise lesen. Lektion 6 greift das auf.
Die nächste Lektion führt das Gespräch von Hand. Damit wird das Protokoll von einer Beschreibung zu etwas, das du selbst verwendet hast.
Aufgabe
Schreibe drei kleine Funktionen, die ein Iterable und einen Iterator anhand ihres Verhaltens unterscheiden, nicht anhand ihres Typnamens.
walks_twice(items)gibt ein Tupel aus zwei Listen zurück: alles aus einem ersten und alles aus einem zweiten Durchlauf. Eine Liste ergibt zwei volle Listen, ein Iterator eine volle und eine leere.is_own_iterator(items)meldet, ob die Anfrage nach einem Iterator das Objekt selbst zurückgibt. Bei einem Iterator ist das wahr, bei einer Liste falsch.safe_count(items)gibt die Anzahl der Elemente zurück und muss für beide funktionieren. Eine Lösung, die ihre Eingabe zweimal durchläuft, kann das nicht leisten.
Führe das Programm aus, um jede Antwort für eine Liste und einen Iterator über dieselben drei Werte zu sehen.