Python & AI Tutorials Logo
Python Programmierung

35. Wie Iteration funktioniert: Iterables und Iteratoren

Im Laufe dieses Buches haben Sie for-Schleifen verwendet, um über Listen, Strings, Dictionaries und andere Sammlungen zu iterieren. Sie haben unzählige Male Code wie for item in my_list: geschrieben. Aber was passiert eigentlich hinter den Kulissen, wenn Python eine for-Schleife ausführt? Woher weiß Python, wie es durch unterschiedliche Arten von Sammlungen Schritt für Schritt gehen soll?

In diesem Kapitel werden wir Pythons Iterationsprotokoll (engl. iteration protocol) untersuchen – den Mechanismus, der for-Schleifen zum Funktionieren bringt. Sie lernen Iterables (engl. iterables) kennen (Objekte, über die Sie schleifen können) und Iteratoren (engl. iterators) (Objekte, die das tatsächliche Durchlaufen der Werte übernehmen). Wenn Sie diesen Unterschied verstehen, vertiefen Sie Ihr Wissen darüber, wie Python funktioniert, und bereiten sich auf die Arbeit mit Generatoren in Kapitel 36 vor.

35.1) Was es bedeutet, dass ein Objekt iterierbar ist

35.1.1) Das Konzept der Iterierbarkeit

Ein Iterable ist jedes Python-Objekt, über das mit einer for-Schleife iteriert werden kann. Wenn wir sagen „iteriert werden“, meinen wir, dass Python Elemente aus dem Objekt nacheinander einzeln abrufen kann.

Sie haben bereits mit vielen Iterables gearbeitet:

python
# Listen sind iterierbar
numbers = [1, 2, 3, 4, 5]
for num in numbers:
    print(num)  # Output: 1, 2, 3, 4, 5 (on separate lines)
 
# Strings sind iterierbar
text = "Python"
for char in text:
    print(char)  # Output: P, y, t, h, o, n (on separate lines)
 
# Dictionaries sind iterierbar (standardmäßig über Schlüssel)
student = {"name": "Alice", "age": 20, "grade": "A"}
for key in student:
    print(key)  # Output: name, age, grade (on separate lines)

All diese Objekte – Listen, Strings, Dictionaries, Tupel, Sets, Ranges und Dateien – sind Iterables, weil sie Pythons Iterationsprotokoll unterstützen (eine Menge von Regeln, die es Python erlaubt, über sie zu schleifen).

35.1.2) Was ein Objekt iterierbar macht

Damit ein Objekt iterierbar ist, muss es eine spezielle Methode namens __iter__() implementieren. Diese Methode gibt ein Iterator-Objekt zurück. Machen Sie sich noch keine Sorgen um die Details – wir werden Iteratoren im nächsten Abschnitt untersuchen.

Sie können prüfen, ob ein Objekt iterierbar ist, indem Sie versuchen, mit der eingebauten Funktion iter() einen Iterator daraus zu erhalten:

python
# Testen, ob Objekte iterierbar sind
numbers = [1, 2, 3]
iterator = iter(numbers)  # Funktioniert - Listen sind iterierbar
print(type(iterator))  # Output: <class 'list_iterator'>
 
text = "Hello"
iterator = iter(text)  # Funktioniert - Strings sind iterierbar
print(type(iterator))  # Output: <class 'str_iterator'>
 
# Versuch mit einem nicht iterierbaren Objekt
value = 42
try:
    iterator = iter(value)  # Schlägt fehl - Integer sind nicht iterierbar
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: 'int' object is not iterable

Wenn Sie iter() auf ein iterierbares Objekt anwenden, ruft Python die Methode __iter__() des Objekts auf und gibt einen Iterator zurück. Wenn das Objekt diese Methode nicht hat, erhalten Sie einen TypeError.

35.1.3) Iterables vs. Sequenzen

Es ist wichtig zu verstehen, dass nicht alle Iterables Sequenzen sind. Eine Sequenz ist eine spezielle Art von Iterable, die Indexierung unterstützt und eine definierte Reihenfolge hat.

python
# Sequenzen unterstützen Indexierung
my_list = [10, 20, 30]
print(my_list[0])  # Output: 10
 
my_string = "Python"
print(my_string[2])  # Output: t
 
# Sets sind iterierbar, aber KEINE Sequenzen (keine Indexierung, keine garantierte Reihenfolge)
my_set = {1, 2, 3}
for item in my_set:
    print(item)  # Funktioniert - Sets sind iterierbar
 
# Aber Indexierung funktioniert nicht
try:
    print(my_set[0])  # Schlägt fehl - Sets unterstützen keine Indexierung
except TypeError as e:
    print(f"Error: {e}")  # Output: Error: 'set' object is not subscriptable

Wichtiger Unterschied: Alle Sequenzen (Listen, Tupel, Strings, Ranges) sind Iterables, aber nicht alle Iterables sind Sequenzen. Sets und Dictionaries sind Iterables, aber keine Sequenzen, weil sie keine Indexierung unterstützen.

Python-Objekte

Iterables

Nicht-Iterables

Sequenzen

Nicht-Sequenz-Iterables

Listen, Tupel, Strings, Ranges

Sets, Dictionaries, Dateien

Zahlen, None, Booleans

35.1.4) Warum Iterierbarkeit wichtig ist

Wenn Sie Iterierbarkeit verstehen, hilft Ihnen das dabei:

  1. Zu wissen, worüber Sie schleifen können: Jedes Iterable funktioniert mit for-Schleifen
  2. Fehlermeldungen zu verstehen: „object is not iterable“ bedeutet, dass Sie es nicht in einer for-Schleife verwenden können
  3. Comprehensions zu nutzen: Listen-, Set- und Dictionary-Comprehensions funktionieren mit jedem Iterable
  4. Mit eingebauten Funktionen zu arbeiten: Viele Built-ins wie sum(), max(), min() und sorted() akzeptieren jedes Iterable
python
# All dies funktioniert, weil es Iterables akzeptiert
numbers = [1, 2, 3, 4, 5]
print(sum(numbers))  # Output: 15
 
text = "Python"
print(max(text))  # Output: y (highest alphabetically)
 
# Funktioniert sogar mit Sets
unique_values = {10, 5, 20, 15}
print(sorted(unique_values))  # Output: [5, 10, 15, 20]

35.2) Alltägliche Iteratoren in Python (Dateien, Ranges, Dictionaries und mehr)

35.2.1) Was ist ein Iterator

Ein Iterator ist ein Objekt, das einen Datenstrom repräsentiert. Es gibt jeweils einen Wert zurück, wenn Sie nach dem nächsten Element fragen. Sobald ein Iterator alle seine Werte zurückgegeben hat, ist er erschöpft und kann nicht wiederverwendet werden.

Stellen Sie sich einen Iterator wie ein Lesezeichen in einem Buch vor:

  • Er merkt sich, wo Sie in der Sequenz sind
  • Sie können nach dem nächsten Element fragen
  • Sobald Sie am Ende sind, können Sie nicht zurück, ohne einen neuen Iterator zu erzeugen

Der entscheidende Unterschied zwischen einem Iterable und einem Iterator:

  • Ein Iterable ist etwas, über das Sie iterieren können (wie eine Liste)
  • Ein Iterator ist das Objekt, das das Iterieren tatsächlich ausführt (der Mechanismus, der durch die Liste schrittweise vorangeht)
python
# Eine Liste ist ein Iterable
numbers = [1, 2, 3]
 
# Einen Iterator aus dem Iterable holen
iterator = iter(numbers)
 
# Der Iterator ist ein separates Objekt
print(type(numbers))    # Output: <class 'list'>
print(type(iterator))   # Output: <class 'list_iterator'>

35.2.2) Iteratoren in for-Schleifen

Wenn Sie eine for-Schleife schreiben, erzeugt Python automatisch im Hintergrund einen Iterator:

python
numbers = [10, 20, 30]
 
# Was Sie schreiben:
for num in numbers:
    print(num)
 
# Was Python intern macht (konzeptionell):
# 1. iter(numbers) aufrufen, um einen Iterator zu bekommen
# 2. Wiederholt next() auf dem Iterator aufrufen
# 3. Stoppen, wenn der Iterator StopIteration auslöst

So sieht das explizit aus:

python
numbers = [10, 20, 30]
 
# Manuelles Iterieren (was for automatisch macht)
iterator = iter(numbers)
try:
    print(next(iterator))  # Output: 10
    print(next(iterator))  # Output: 20
    print(next(iterator))  # Output: 30
    print(next(iterator))  # Would raise StopIteration
except StopIteration:
    print("No more items")  # Output: No more items

Die for-Schleife behandelt die Ausnahme StopIteration automatisch, weshalb Sie sie in normalem Code nie sehen.

Ja

Nein -> StopIteration

for item in iterable:

Python ruft iter(iterable) auf

Erhält Iterator-Objekt

Python ruft next(iterator) auf

Weitere Elemente?

Weise item zu

Schleifenrumpf ausführen

Schleife verlassen

35.2.3) Dateiobjekte als Iteratoren

Dateiobjekte sind hervorragende Beispiele für Iteratoren. Wenn Sie über eine Datei iterieren, liest sie jeweils eine Zeile:

python
# Eine Beispieldatei erstellen
with open("students.txt", "w") as file:
    file.write("Alice\n")
    file.write("Bob\n")
    file.write("Charlie\n")
 
# Die Datei Zeile für Zeile lesen
with open("students.txt", "r") as file:
    for line in file:
        print(line.strip())  # Output: Alice, Bob, Charlie (on separate lines)

Dateiobjekte sind sowohl Iterables als auch Iteratoren. Sie geben sich selbst zurück, wenn Sie iter() auf sie anwenden:

python
with open("students.txt", "r") as file:
    iterator = iter(file)
    print(file is iterator)  # Output: True (same object)
    
    # Zeilen manuell lesen
    print(next(iterator))  # Output: Alice
    print(next(iterator))  # Output: Bob
    print(next(iterator))  # Output: Charlie

Das ist speichereffizient, weil Python nicht die gesamte Datei in den Speicher lädt – es liest jeweils eine Zeile, wenn Sie sie anfordern.

35.2.4) Range-Objekte als Iteratoren

Range-Objekte sind Iterables, die Zahlen bei Bedarf erzeugen:

python
# Ein range ist ein Iterable
numbers = range(1, 4)
print(type(numbers))  # Output: <class 'range'>
 
# Einen Iterator aus dem range holen
iterator = iter(numbers)
print(type(iterator))  # Output: <class 'range_iterator'>
 
# Den Iterator verwenden
print(next(iterator))  # Output: 1
print(next(iterator))  # Output: 2
print(next(iterator))  # Output: 3

Ranges sind speichereffizient, weil sie nicht alle Zahlen im Speicher ablegen – sie berechnen jede Zahl erst, wenn sie angefordert wird:

python
# Dieser range repräsentiert 1 Million Zahlen, verwendet aber minimalen Speicher
large_range = range(1000000)
print(type(large_range))  # Output: <class 'range'>
 
# Einen Iterator holen
iterator = iter(large_range)
print(next(iterator))  # Output: 0
print(next(iterator))  # Output: 1
# ... kann für 1 Million Werte fortgesetzt werden

35.2.5) Dictionary-Iteratoren

Dictionaries stellen unterschiedliche Iteratoren für Schlüssel, Werte und Einträge bereit:

python
student = {"name": "Alice", "age": 20, "grade": "A"}
 
# Über Schlüssel iterieren (Standard)
for key in student:
    print(key)  # Output: name, age, grade (on separate lines)
 
# Explizit einen Schlüssel-Iterator holen
keys_iterator = iter(student.keys())
print(next(keys_iterator))  # Output: name
print(next(keys_iterator))  # Output: age
 
# Über Werte iterieren
values_iterator = iter(student.values())
print(next(values_iterator))  # Output: Alice
print(next(values_iterator))  # Output: 20
 
# Über Items iterieren (Schlüssel-Wert-Paare)
items_iterator = iter(student.items())
print(next(items_iterator))  # Output: ('name', 'Alice')
print(next(items_iterator))  # Output: ('age', 20)

35.2.6) Iteratoren sind erschöpfbar

Eine entscheidende Eigenschaft von Iteratoren ist, dass sie nur einmal verwendet werden können. Sobald sie erschöpft sind, setzen sie sich nicht zurück:

python
numbers = [1, 2, 3]
iterator = iter(numbers)
 
# Erster Durchlauf durch den Iterator
print(next(iterator))  # Output: 1
print(next(iterator))  # Output: 2
print(next(iterator))  # Output: 3
 
# Iterator ist jetzt erschöpft
try:
    print(next(iterator))  # Raises StopIteration
except StopIteration:
    print("Iterator exhausted")  # Output: Iterator exhausted
 
# Um erneut zu iterieren, einen neuen Iterator erstellen
iterator = iter(numbers)
print(next(iterator))  # Output: 1 (fresh start)

Das unterscheidet sich vom Iterable selbst, über das mehrfach iteriert werden kann:

python
numbers = [1, 2, 3]
 
# Erste Iteration
for num in numbers:
    print(num)  # Output: 1, 2, 3
 
# Zweite Iteration (funktioniert problemlos - erstellt einen neuen Iterator)
for num in numbers:
    print(num)  # Output: 1, 2, 3

35.3) iter() und next() verwenden, um durch Iterables zu iterieren

35.3.1) Die Funktion iter()

Die Funktion iter() nimmt ein Iterable und gibt einen Iterator zurück. Das ist der erste Schritt im Iterationsprotokoll:

python
# Iteratoren aus unterschiedlichen Iterables erstellen
numbers = [10, 20, 30]
iterator = iter(numbers)
print(type(iterator))  # Output: <class 'list_iterator'>
 
text = "Hi"
text_iterator = iter(text)
print(type(text_iterator))  # Output: <class 'str_iterator'>
 
my_set = {1, 2, 3}
set_iterator = iter(my_set)
print(type(set_iterator))  # Output: <class 'set_iterator'>

Jeder Iterable-Typ gibt seinen eigenen spezialisierten Iterator-Typ zurück, aber sie funktionieren alle auf die gleiche Weise – Sie rufen next() auf, um den nächsten Wert zu erhalten.

35.3.2) Die Funktion next()

Die Funktion next() ruft das nächste Element aus einem Iterator ab. Wenn es keine weiteren Elemente gibt, löst sie StopIteration aus:

python
colors = ["red", "green", "blue"]
iterator = iter(colors)
 
# Elemente einzeln abrufen
print(next(iterator))  # Output: red
print(next(iterator))  # Output: green
print(next(iterator))  # Output: blue
 
# Keine weiteren Elemente
try:
    print(next(iterator))  # Raises StopIteration
except StopIteration:
    print("No more colors")  # Output: No more colors

35.3.3) Einen Standardwert an next() übergeben

Sie können next() als zweites Argument einen Standardwert übergeben. Wenn der Iterator erschöpft ist, wird next() statt einer StopIteration-Exception den von Ihnen angegebenen Standardwert zurückgeben:

python
numbers = [1, 2, 3]
iterator = iter(numbers)
 
print(next(iterator))           # Output: 1
print(next(iterator))           # Output: 2
print(next(iterator))           # Output: 3
print(next(iterator, "Done"))   # Output: Done (default value, no exception)
print(next(iterator, "Done"))   # Output: Done (still exhausted)

Das ist nützlich, wenn Sie das Ende der Iteration sauber behandeln möchten, ohne Exception-Handling:

35.4) Benutzerdefinierte Iteratoren mit iter und next erstellen

35.4.1) Warum benutzerdefinierte Iteratoren erstellen

Pythons eingebaute Iterables (Listen, Strings, Dateien) decken die meisten gängigen Fälle ab. Manchmal müssen Sie jedoch eigene iterierbare Objekte für spezielles Verhalten erstellen:

  • Sequenzen mit benutzerdefinierter Logik erzeugen
  • Über Datenstrukturen iterieren, die Sie entwerfen
  • Speichereffiziente Iteration über große Datensätze erstellen
  • Lazy Evaluation implementieren (Werte nur berechnen, wenn sie benötigt werden)

Um einen benutzerdefinierten Iterator zu erstellen, müssen zwei spezielle Methoden implementiert werden: __iter__() und __next__().

35.4.2) Das Iterator-Protokoll

Damit ein Objekt ein Iterator ist, muss es implementieren:

  1. __iter__(): Gibt das Iterator-Objekt selbst zurück (meist self)
  2. __next__(): Gibt den nächsten Wert in der Sequenz zurück oder löst StopIteration aus, wenn es fertig ist
python
class SimpleCounter:
    """Ein Iterator, der von start bis end zählt."""
    
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        """Gibt das Iterator-Objekt zurück (self)."""
        return self
    
    def __next__(self):
        """Gibt den nächsten Wert zurück oder löst StopIteration aus."""
        if self.current > self.end:
            raise StopIteration
        
        value = self.current
        self.current += 1
        return value
 
# Den benutzerdefinierten Iterator verwenden
counter = SimpleCounter(1, 5)
 
for num in counter:
    print(num)
# Output: 1
# Output: 2
# Output: 3
# Output: 4
# Output: 5

Schauen wir uns an, was passiert:

  1. Die for-Schleife ruft iter(counter) auf, was counter.__iter__() aufruft und counter selbst zurückbekommt
  2. Die Schleife ruft wiederholt next(counter) auf, was counter.__next__() aufruft
  3. Jeder Aufruf von __next__() gibt die nächste Zahl zurück und erhöht current
  4. Wenn current > end, löst __next__() StopIteration aus, und die Schleife stoppt

35.4.3) Manuelle Nutzung benutzerdefinierter Iteratoren

Sie können benutzerdefinierte Iteratoren auch manuell mit iter() und next() verwenden:

python
counter = SimpleCounter(10, 13)
 
# Den Iterator holen (gibt sich selbst zurück)
iterator = iter(counter)
print(iterator is counter)  # Output: True
 
# Werte manuell abrufen
print(next(iterator))  # Output: 10
print(next(iterator))  # Output: 11
print(next(iterator))  # Output: 12
print(next(iterator))  # Output: 13
 
# Jetzt erschöpft
try:
    print(next(iterator))
except StopIteration:
    print("Counter exhausted")  # Output: Counter exhausted

35.4.4) Iteratoren sind erschöpfbar (noch einmal)

Denken Sie daran, dass Iteratoren nur einmal verwendet werden können:

python
counter = SimpleCounter(1, 3)
 
# Erste Iteration
for num in counter:
    print(num)  # Output: 1, 2, 3
 
# Zweite Iteration (funktioniert nicht - Iterator ist erschöpft)
for num in counter:
    print(num)  # Nothing printed - iterator is already exhausted

Um erneut zu iterieren, müssen Sie eine neue Instanz erstellen:

python
# Für jede Iteration einen neuen Counter erstellen
for num in SimpleCounter(1, 3):
    print(num)  # Output: 1, 2, 3
 
for num in SimpleCounter(1, 3):
    print(num)  # Output: 1, 2, 3 (new iterator)

35.4.5) Eine Iterable-Klasse erstellen (nicht nur einen Iterator)

Oft möchten Sie eine Klasse, die iterierbar ist, aber jedes Mal einen frischen Iterator erstellt. Dazu trennen Sie das Iterable vom Iterator:

python
class CounterIterable:
    """Ein Iterable, das frische Counter-Iteratoren erstellt."""
    
    def __init__(self, start, end):
        self.start = start
        self.end = end
    
    def __iter__(self):
        """Gibt jedes Mal einen neuen Iterator zurück."""
        return CounterIterator(self.start, self.end)
 
class CounterIterator:
    """Der eigentliche Iterator, der das Zählen übernimmt."""
    
    def __init__(self, start, end):
        self.current = start
        self.end = end
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current > self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value
 
# Jetzt können wir mehrfach iterieren
counter = CounterIterable(1, 3)
 
# Erste Iteration
for num in counter:
    print(num)  # Output: 1, 2, 3
 
# Zweite Iteration (funktioniert, weil __iter__ einen neuen Iterator erstellt)
for num in counter:
    print(num)  # Output: 1, 2, 3

Dieses Muster trennt Zuständigkeiten:

  • CounterIterable ist das Iterable – es weiß, wie man Iteratoren erstellt
  • CounterIterator ist der Iterator – er weiß, wie man durch Werte schrittweise geht

35.4.6) Praktisches Beispiel: Über eine benutzerdefinierte Datenstruktur iterieren

Erstellen wir einen Iterator für eine benutzerdefinierte Datenstruktur – eine einfache Playlist:

python
class Playlist:
    """Eine Musik-Playlist, über die iteriert werden kann."""
    
    def __init__(self):
        self.songs = []
    
    def add_song(self, title, artist):
        """Einen Song zur Playlist hinzufügen."""
        self.songs.append({"title": title, "artist": artist})
    
    def __iter__(self):
        """Gibt einen Iterator für die Playlist zurück."""
        return PlaylistIterator(self.songs)
 
class PlaylistIterator:
    """Iterator, um durch Songs in einer Playlist zu gehen."""
    
    def __init__(self, songs):
        self.songs = songs
        self.index = 0
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index >= len(self.songs):
            raise StopIteration
        
        song = self.songs[self.index]
        self.index += 1
        return song
 
# Die Playlist verwenden
playlist = Playlist()
playlist.add_song("Imagine", "John Lennon")
playlist.add_song("Bohemian Rhapsody", "Queen")
playlist.add_song("Hotel California", "Eagles")
 
# Über Songs iterieren
print("Now playing:")
for song in playlist:
    print(f"  {song['title']} by {song['artist']}")
# Output: Now playing:
# Output:   Imagine by John Lennon
# Output:   Bohemian Rhapsody by Queen
# Output:   Hotel California by Eagles
 
# Kann erneut iterieren (erstellt einen neuen Iterator)
print("\nReplay:")
for song in playlist:
    print(f"  {song['title']}")
# Output: Replay:
# Output:   Imagine
# Output:   Bohemian Rhapsody
# Output:   Hotel California

35.4.7) Wann man benutzerdefinierte Iteratoren verwenden sollte

Erstellen Sie benutzerdefinierte Iteratoren, wenn:

  1. Sie Lazy Evaluation brauchen: Werte bei Bedarf erzeugen, statt sie alle zu speichern
  2. Sie eine benutzerdefinierte Datenstruktur haben: Machen Sie sie iterierbar, damit sie mit for-Schleifen funktioniert
  3. Sie spezielle Iterationslogik brauchen: Elemente überspringen, Werte transformieren oder komplexes schrittweises Durchlaufen implementieren
  4. Speichereffizienz wichtig ist: Große Sequenzen erzeugen, ohne sie zu speichern

Allerdings lernen Sie in Kapitel 36 Generatoren kennen, die eine viel einfachere Möglichkeit bieten, Iteratoren mit dem Schlüsselwort yield zu erstellen. Generatoren werden üblicherweise gegenüber dem manuellen Implementieren von __iter__() und __next__() bevorzugt, weil sie kompakter und leichter zu verstehen sind.

Wenn Sie verstehen, wie man benutzerdefinierte Iteratoren erstellt, bekommen Sie Einblick in die Funktionsweise von Pythons Iterationsprotokoll, selbst wenn Sie oft stattdessen Generatoren verwenden werden. Die Konzepte, die Sie hier gelernt haben – __iter__(), __next__() und StopIteration – sind grundlegend, um Generatoren und andere fortgeschrittene Iterationstechniken im nächsten Kapitel zu verstehen.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai