Zum Inhalt springen
JAY IMDAHL
App-Entwicklung 10 min

Python Iterables und Iteratoren

Warum kann man mit derselben for-Schleife durch Listen, Dicts, Strings und Dateien laufen? Und wie spart ein Iterator Gigabyte an Speicher? Ein Deep-Dive ins Iterator-Protokoll.

VIDEO · BINÄRVERKEHR
video-pendant zum tutorial auf dem binärverkehr-kanal

Wer Python schreibt, benutzt jeden Tag den gleichen Schleifen-Kopf:

for frucht in ["Apfel", "Banane", "Kiwi"]:
    print(frucht)

for taste, wert in {"Apfel": 8, "Banane": 1}.items():
    print(taste, wert)

for zeile in open("fruechte.txt"):
    print(zeile.strip())

Drei völlig unterschiedliche Objekte: eine Liste im Speicher, ein Dictionary mit Hash-Tabelle, eine Datei, die noch gar nicht eingelesen ist. Und trotzdem funktioniert dieselbe for-Schleife für alles. Bei einem int dagegen kracht es:

for x in 22:
    print(x)
# TypeError: 'int' object is not iterable

Der Grund ist nicht, dass eine Liste „mehr Elemente hat” und eine Zahl nicht. Der Grund ist eine Schnittstelle, die Python erwartet, das Iterator-Protokoll. In diesem Post schauen wir uns an, wie es funktioniert, warum Python sauber zwischen Iterable und Iterator trennt, und wie genau dieses Protokoll es möglich macht, dass du ein 50-GB-Logfile auf einem Rechner mit 8 GB RAM verarbeitest.

1. Was eine for-Schleife wirklich macht

Eine for-Schleife sieht aus wie ein eingebautes Sprachkonstrukt. In Wirklichkeit ist sie syntaktischer Zucker für zwei explizite Aufrufe: erst iter(...), dann wiederholt next(...).

Das Folgende ist äquivalent:

for frucht in ["Apfel", "Banane", "Kiwi"]:
    print(frucht)
it = iter(["Apfel", "Banane", "Kiwi"])
while True:
    try:
        frucht = next(it)
    except StopIteration:
        break
    print(frucht)

Damit ist die Frage „warum funktioniert die Schleife mit so vielen Typen?” beantwortet: Sie funktioniert mit allem, das auf iter(...) einen Iterator zurückgibt, der seinerseits auf next(...) Werte liefert und am Ende StopIteration wirft. Genau das ist das Protokoll.

2. Iterable vs. Iterator

Zwei Wörter, die fast gleich aussehen, aber zwei verschiedene Dinge meinen. Diese Trennung sauber zu verstehen ist der ganze Punkt dieses Posts.

  • Iterable — ein Objekt, durch dessen Elemente man iterieren kann. Listen, Strings, Dicts, Dateien, eigene Klassen mit __iter__. Hält die Daten.
  • Iterator — ein Objekt, das den aktuellen Stand der Iteration kennt und auf Anfrage das nächste Element liefert. Hält die Position.

Aus einem Iterable bekommst du einen Iterator, indem du iter(...) aufrufst. Aus einem Iterator bekommst du Werte, indem du next(...) aufrufst. Schematisch:

Iterable  --iter()-->  Iterator  --next()-->  Element
                       Iterator  --next()-->  Element
                       Iterator  --next()-->  StopIteration

Das Iterable kann beliebig oft neue Iteratoren ausgeben. Jeder Iterator ist einweg — einmal durchgelaufen, ist er leer.

liste = ["Apfel", "Banane", "Kiwi"]

it1 = iter(liste)
it2 = iter(liste)

print(next(it1))  # Apfel
print(next(it1))  # Banane
print(next(it2))  # Apfel    — eigener, unabhängiger Zustand

Zwei Iteratoren, dieselbe Liste, kein gemeinsamer Stand. Genau deshalb gibt es die Trennung, dazu in §5 mehr.

3. Das Protokoll im Detail

Damit ein Objekt mitspielen kann, muss es die richtigen Dunder-Methoden implementieren. Die Python-Dokumentation drückt es nüchtern aus: Ein Iterable hat __iter__, ein Iterator hat __next__.

MethodeWer implementiertWas sie tut
__iter__(self)IterableGibt einen frischen Iterator zurück
__next__(self)IteratorGibt das nächste Element zurück, sonst raise StopIteration

Die globalen Funktionen iter(x) und next(x) rufen intern genau diese Methoden auf. iter(x) ist nichts weiter als ein etwas eleganteres x.__iter__(), next(x) entsprechend x.__next__().

Wenn du das einmal direkt sehen willst:

liste = ["Apfel", "Banane", "Kiwi"]
it = iter(liste)

print(type(it))   # <class 'list_iterator'>
print(next(it))   # Apfel
print(next(it))   # Banane
print(next(it))   # Kiwi
print(next(it))   # StopIteration

Die list ist das Iterable, das list_iterator ist der dazugehörige Iterator. Zwei verschiedene Klassen mit klar getrennten Aufgaben.

4. Selbst gebaut — ein Iterable mit eigenem Iterator

Theorie reicht. Wir bauen das von Hand nach, zuerst ein Iterable, dann den Iterator dazu.

Das Iterable

class FrüchteIterable:
    def __init__(self):
        self.daten = ["Apfel", "Banane", "Kiwi"]

    def __iter__(self):
        return FrüchteIterator(self.daten)

Mehr braucht ein Iterable nicht: eine Datenquelle und eine __iter__-Methode, die einen frischen Iterator zurückgibt. Wichtig ist, dass der Iterator nur eine Referenz auf die Daten bekommt, keine Kopie. Mehrere Iteratoren teilen sich also dieselbe Liste, aber nicht denselben Fortschritt.

Der Iterator

class FrüchteIterator:
    def __init__(self, daten_referenz):
        self.daten = daten_referenz
        self.aktuell = 0

    def __next__(self):
        if self.aktuell >= len(self.daten):
            raise StopIteration
        wert = self.daten[self.aktuell]
        self.aktuell += 1
        return wert

Drei Dinge passieren hier:

  1. Endprüfung zuerst. Wenn der Index außerhalb der Liste liegt, raise StopIteration. Die for-Schleife fängt das ab und bricht ab.
  2. Aktuellen Wert holen.
  3. Position fortzählen und Wert zurückgeben. Die Reihenfolge ist wichtig: erst den Wert merken, dann den Index erhöhen, dann den gemerkten Wert zurückgeben. Sonst überspringt die Schleife das erste Element.

Probieren wir es aus:

for frucht in FrüchteIterable():
    print(frucht)
# Apfel
# Banane
# Kiwi

Funktioniert wie eine eingebaute Liste. Die for-Schleife sieht keinen Unterschied — sie ruft __iter__ auf, kriegt einen FrüchteIterator und ruft __next__ bis zur StopIteration.

5. Warum diese Trennung?

An dieser Stelle drängt sich eine Frage auf: Warum zwei Klassen? Warum kann das Iterable nicht selbst die Position halten?

Die Antwort ist parallele Iteration. Stell dir vor, das Iterable hält den Stand selbst. Dann würde Folgendes nicht mehr funktionieren:

früchte = FrüchteIterable()

for a in früchte:
    for b in früchte:
        print(a, b)

Mit dem aktuellen Design erzeugen die beiden Schleifen jeweils ihren eigenen Iterator, der seinen eigenen Index führt. Würde der Stand am Iterable kleben, wären beide Schleifen verkoppelt und das Verhalten kaputt.

Dieselbe Trennung erlaubt es zwei Funktionen, gleichzeitig durch dieselbe Datenquelle zu laufen, ohne dass sie sich gegenseitig stören. „Separation of Concerns” als Sprachfeature: das Iterable hält die Daten, der Iterator hält den Fortschritt.

6. Der Selbstbezug: Iterator ist auch ein Iterable

Eine kleine, aber wichtige Spitzfindigkeit. Die for-Schleife erwartet ein Iterable. Was passiert, wenn du ihr stattdessen direkt einen Iterator gibst?

it = iter([1, 2, 3])

for x in it:
    print(x)

Funktioniert. Aber laut Protokoll dürfte die for-Schleife nur Iterables annehmen, und unser Iterator hat doch nur __next__?

Die Lösung ist eine Konvention: Jeder Iterator ist gleichzeitig ein Iterable, der sich selbst zurückgibt. Mit einer einzigen Zeile:

class FrüchteIterator:
    def __init__(self, daten_referenz):
        self.daten = daten_referenz
        self.aktuell = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.aktuell >= len(self.daten):
            raise StopIteration
        wert = self.daten[self.aktuell]
        self.aktuell += 1
        return wert

Damit kann die for-Schleife dem Iterator dasselbe iter(...) zumuten wie einem Iterable und kriegt einfach den Iterator selbst zurück. Das macht es möglich, einen Iterator pausierend zu konsumieren, ihn weiterzureichen, ihn nochmal in eine Schleife zu stecken, weil er sein eigenes Iterable ist.

Was die Beziehung nicht umkehrt: Nicht jedes Iterable ist ein Iterator. Eine Liste ist ein Iterable, aber sie hat kein __next__. Wer das verwechselt, baut sich gerne ein Iterable, das nach dem ersten Durchlauf leer ist, weil es nebenbei zum Iterator gemacht wurde.

Faustregel: Iteratoren sind immer auch Iterables. Iterables sind nicht automatisch Iteratoren. Asymmetrisch.

7. Lazy vs. Eager — der eigentliche Hebel

Bisher war das Iterator-Protokoll eine elegante Abstraktion. Der praktische Gewinn kommt erst, wenn das Iterable die Werte nicht im Voraus berechnet, sondern erst dann, wenn __next__ aufgerufen wird. Das nennt man Lazy Evaluation.

Vergleich: Eine Million Zahlen, einmal eager als Liste, einmal lazy als Iterator.

import sys

eager = [i for i in range(1_000_000)]
lazy = iter(range(1_000_000))

print(sys.getsizeof(eager))  # ~8 MB
print(sys.getsizeof(lazy))   # ~200 Byte

Die Liste materialisiert alle eine Million int-Objekte im Speicher. Der Iterator hält nur einen Zähler und die Anweisung, wie der nächste Wert berechnet wird, wenn er gefragt wird. Vierzigtausendfacher Unterschied im Speicher, ohne dass sich am Schleifen-Code etwas ändert.

Das ist der Mechanismus, der es erlaubt, ein 50-GB-Logfile auf einem 8-GB-RAM-Rechner zu verarbeiten:

with open("riesig.log") as f:
    for zeile in f:
        if "ERROR" in zeile:
            print(zeile.strip())

Das Dateiobjekt ist ein Iterator. Es liest immer nur die nächste Zeile in den Speicher, gibt sie zurück, und vergisst sie. Egal wie groß die Datei wird, der Speicherverbrauch bleibt konstant.

Dasselbe Prinzip steckt hinter Generators (yield), range(...), map(...), filter(...), enumerate(...), zip(...). Alle geben Iteratoren zurück, alle produzieren ihre Werte erst auf Anfrage.

8. Wo das im Alltag auftaucht

Du musst selten von Hand einen Iterator-Klassen-Codeblock schreiben. Aber das Protokoll wirkt überall:

  • Generators: def f(): yield 1; yield 2 ist ein Iterator, der Python für dich baut. Das yield ist quasi ein return, das die Funktion an der Stelle einfriert, statt sie zu beenden.
  • Comprehensions: Listen-Comprehensions sind eager, Generator-Comprehensions lazy. Klammern entscheiden: [x*x for x in range(N)] vs. (x*x for x in range(N)).
  • Standard-Library: itertools ist ein ganzer Werkzeugkasten an fertigen Iteratoren — chain, groupby, accumulate, cycle. Lazy, kombinierbar, billig.
  • Dateien und Sockets: alles, wo Daten erst ankommen, wenn du danach fragst.
  • Netzwerk-Pagination: ein Iterator, der nach next(...) die nächste Seite vom Server holt, ist die saubere Art, eine API mit Cursor-basiertem Paging zu konsumieren.

Sobald du das Protokoll im Kopf hast, fällt dir auf, wie viel Python-Idiomatik darauf aufbaut.

9. Komplett-Beispiel: ein lazy LineReader für große Dateien

Zum Abschluss etwas, das die ganze Mechanik in einem realistischen Stück Code zeigt: ein Iterator, der eine Datei liest, leere Zeilen überspringt und führende/abschließende Whitespaces wegmacht, alles lazy. So etwas baut man tatsächlich, wenn man große Logs oder CSVs streamt.

class CleanLineReader:
    """Liest eine Datei Zeile für Zeile, überspringt leere Zeilen,
    strippt Whitespace. Kein Wort der Datei landet je gemeinsam
    im Speicher."""

    def __init__(self, pfad):
        self.pfad = pfad
        self._datei = None

    def __iter__(self):
        # Jeder iter()-Aufruf öffnet die Datei frisch.
        # Damit lässt sich das Iterable mehrfach durchlaufen.
        self._datei = open(self.pfad, "r", encoding="utf-8")
        return self

    def __next__(self):
        if self._datei is None:
            raise StopIteration
        while True:
            zeile = self._datei.readline()
            if zeile == "":
                # Dateiende erreicht.
                self._datei.close()
                self._datei = None
                raise StopIteration
            zeile = zeile.strip()
            if zeile:
                return zeile


for zeile in CleanLineReader("riesig.log"):
    if "ERROR" in zeile:
        print(zeile)

Drei Beobachtungen lohnen sich:

  • Iterator und Iterable in einem. __iter__ gibt self zurück, __next__ liefert das nächste Element. Klassisches Pattern für „eigentlich ein Iterator, aber ich will ihn auch direkt in eine for-Schleife stecken können”.
  • Frische Iteration durch frisches Öffnen. Weil __iter__ die Datei jedes Mal neu öffnet, kannst du dasselbe CleanLineReader-Objekt mehrfach durchlaufen. Würden wir die Datei in __init__ öffnen, wäre nach dem ersten Durchlauf Schluss.
  • Speicherverbrauch ist konstant. Egal ob die Datei 50 KB oder 50 GB groß ist, im Speicher liegt immer nur die aktuelle Zeile. Genau das, was du für Streaming-Workloads brauchst.

In der Praxis schreibt man so etwas natürlich als Generator-Funktion, weniger Boilerplate, dasselbe Verhalten:

def clean_lines(pfad):
    with open(pfad, "r", encoding="utf-8") as f:
        for zeile in f:
            zeile = zeile.strip()
            if zeile:
                yield zeile

Aber der Klassen-Weg zeigt das Protokoll vollständig. Generators sind nur die abgekürzte Form davon.

Zusammenfassung

  • Iterable vs. Iterator: das Iterable hält die Daten, der Iterator hält den Fortschritt. iter(iterable) erzeugt den Iterator, next(iterator) liefert das nächste Element.
  • Das Protokoll: ein Iterable hat __iter__, ein Iterator hat __next__ und wirft am Ende StopIteration. Die for-Schleife ruft beides im Hintergrund auf.
  • Trennung erlaubt parallele Iteration: weil jeder iter(...)-Aufruf einen frischen Iterator liefert, können mehrere Schleifen unabhängig durch dieselbe Datenquelle laufen.
  • Iterator als Iterable: jeder Iterator hat in der Regel auch __iter__, das self zurückgibt. Iterables hingegen sind nicht automatisch Iteratoren.
  • Lazy Evaluation: weil __next__ Werte erst bei Bedarf produziert, kann ein Iterator über Millionen oder Milliarden Elemente laufen, ohne sie gleichzeitig im Speicher zu halten.
  • In der Praxis: Generators, Comprehensions, itertools, Dateien, range, zip — überall steckt das Protokoll drin, oft ohne dass das Wort Iterator je fällt.

Wenn du das nächste Mal eine for-Schleife schreibst, weißt du, was unter der Haube passiert. Und du weißt, warum dein 50-GB-Logfile durchläuft, obwohl dein RAM das nie aushalten würde.

Ähnliche Posts

Alle anzeigen
Closures einfach erklärt Thumbnail
App-Entwicklung pythonclosures

Python Closures: Funktionen mit Gedächtnis

Closures sind das Geheimnis hinter Decorators, Factories und elegantem State-Handling in Python. Ein praxisnaher Deep-Dive: wie sie funktionieren und wann du sie brauchst.

9 min
Thumbnail mit Text "Dekoratoren einfach erklärt"
App-Entwicklung pythondekoratoren

Python-Dekoratoren einfach erklärt

Dekoratoren gehören zu den beliebtesten Sprachfeatures der Programmiersprache Python. Lerne, eigene Dekoratorenzu erstellen, um Boilerplate-Code zu reduzieren.

4 min
Python-Community

Werde Teil der Python-Community

Tausch dich mit anderen Python-Lernenden aus, stell deine Fragen im Forum und schau dir die besten Lernvideos rund um Python an — kostenlos auf Skool.