Iteration als Schnittstelle · Übung
Das Iteratorprotokoll verstehen
iter() und next() bilden die Seite des aufrufenden Codes. Diese Lektion zeigt die andere Seite: Was muss ein Objekt anbieten, damit es durchlaufen werden kann?
Es sind zwei Methoden. Ihre Namenskonvention kennst du bereits.
Die beiden Methoden
__iter__ gibt einen Iterator zurück. Python ruft es auf, wenn etwas einen Iterator anfordert, wie iter() und jede for-Schleife.
__next__ gibt das nächste Element zurück oder löst StopIteration aus, wenn keines übrig ist. Python ruft es auf, wenn etwas ein Element anfordert, wie next().
Ein Iterable definiert __iter__. Ein Iterator definiert beide Methoden, und sein __iter__ gibt ihn selbst zurück.
Hier ist das kleinste vollständige Beispiel:
Nirgends eine Liste. Die Zahlen existieren erst, wenn __next__ sie erzeugt. Die for-Schleife weiß davon nichts und muss es auch nicht.
Langsam lesen
__iter__ gibt self zurück. Genau das macht einen Iterator aus: Er ist der Durchläufer, also liefert eine entsprechende Anfrage ihn selbst. Das ist dasselbe iter(x) is x, das du in Lektion 1 getestet hast.
__next__ erledigt die Arbeit. Es prüft, ob etwas übrig ist, löst andernfalls StopIteration aus, verschiebt die Position und gibt das Element zurück. Jeder Iterator folgt dieser Form.
Beachte, was beim zweiten Durchlauf passiert:
Verbraucht, genau wie der Listeniterator aus Lektion 1. Countdown hält seine Position in self.current, und nichts setzt sie zurück. Es ist ein Iterator und deshalb nur einmal durchlaufbar.
Was muss __next__ tun, wenn nichts mehr übrig ist?
Iterable und Iterator als getrennte Objekte
Countdown ist beides zugleich und deshalb verbrauchbar. Soll ein Objekt wiederholt durchlaufbar sein, trenne die Rollen: Das Iterable erzeugt jedes Mal einen neuen Iterator.
Beide Durchläufe sehen beide Elemente, weil jeder list()-Aufruf einen neuen BankWalker mit eigenem _index erhielt. Der Bestand hält die Daten, der Durchläufer die Position. Genau so ist eine Liste mit ihrem eigenen Iterator angeordnet. Jetzt verstehst du warum.
Schreibe das nicht so
Zwei Klassen und ungefähr zwanzig Zeilen, um eine bereits vorhandene Liste zu durchlaufen. In echtem Code fast nie nötig.
Diese Lektion macht das Protokoll greifbar. Die nächsten beiden zeigen, was du stattdessen tatsächlich schreibst: __iter__, das an etwas bereits Iterierbares delegiert, und Generatoren, die diese gesamte Form aus einer gewöhnlichen Funktion erzeugen.
Die Übung baut einmal einen vollständigen Iterator von Hand.
Aufgabe
Schreibe das Protokoll einmal von Hand, damit es nicht bloß eine Beschreibung bleibt.
Countdown(start) ist ein Iterator, der start, start - 1 und so weiter bis 1 liefert und dann stoppt. Er definiert beide Methoden: __iter__ gibt ihn selbst zurück, __next__ liefert die nächste Zahl oder löst StopIteration aus. Ein Countdown(0) liefert überhaupt nichts.
ScoreLog(scores) ist ein Iterable, kein Iterator. Sein __iter__ gibt bei jeder Anfrage einen neuen LogWalker zurück, sodass sich dasselbe Protokoll zweimal durchlaufen lässt.
LogWalker(scores) ist der Iterator: Er hält die Position, gibt sich aus __iter__ selbst zurück und löst nach dem Ende StopIteration aus.
Die Bewertung durchläuft ein ScoreLog zweimal und erwartet bei beiden Durchläufen alle Elemente. Sie durchläuft einen Countdown zweimal und erwartet beim zweiten nichts. Genau um diesen Unterschied geht es.