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:
# 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:
# 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 iterableWenn 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.
# 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 subscriptableWichtiger 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.
35.1.4) Warum Iterierbarkeit wichtig ist
Wenn Sie Iterierbarkeit verstehen, hilft Ihnen das dabei:
- Zu wissen, worüber Sie schleifen können: Jedes Iterable funktioniert mit
for-Schleifen - Fehlermeldungen zu verstehen: „object is not iterable“ bedeutet, dass Sie es nicht in einer
for-Schleife verwenden können - Comprehensions zu nutzen: Listen-, Set- und Dictionary-Comprehensions funktionieren mit jedem Iterable
- Mit eingebauten Funktionen zu arbeiten: Viele Built-ins wie
sum(),max(),min()undsorted()akzeptieren jedes Iterable
# 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)
# 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:
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östSo sieht das explizit aus:
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 itemsDie for-Schleife behandelt die Ausnahme StopIteration automatisch, weshalb Sie sie in normalem Code nie sehen.
35.2.3) Dateiobjekte als Iteratoren
Dateiobjekte sind hervorragende Beispiele für Iteratoren. Wenn Sie über eine Datei iterieren, liest sie jeweils eine Zeile:
# 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:
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: CharlieDas 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:
# 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: 3Ranges sind speichereffizient, weil sie nicht alle Zahlen im Speicher ablegen – sie berechnen jede Zahl erst, wenn sie angefordert wird:
# 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 werden35.2.5) Dictionary-Iteratoren
Dictionaries stellen unterschiedliche Iteratoren für Schlüssel, Werte und Einträge bereit:
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:
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:
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, 335.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:
# 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:
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 colors35.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:
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:
__iter__(): Gibt das Iterator-Objekt selbst zurück (meistself)__next__(): Gibt den nächsten Wert in der Sequenz zurück oder löstStopIterationaus, wenn es fertig ist
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: 5Schauen wir uns an, was passiert:
- Die
for-Schleife ruftiter(counter)auf, wascounter.__iter__()aufruft undcounterselbst zurückbekommt - Die Schleife ruft wiederholt
next(counter)auf, wascounter.__next__()aufruft - Jeder Aufruf von
__next__()gibt die nächste Zahl zurück und erhöhtcurrent - Wenn
current > end, löst__next__()StopIterationaus, und die Schleife stoppt
35.4.3) Manuelle Nutzung benutzerdefinierter Iteratoren
Sie können benutzerdefinierte Iteratoren auch manuell mit iter() und next() verwenden:
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 exhausted35.4.4) Iteratoren sind erschöpfbar (noch einmal)
Denken Sie daran, dass Iteratoren nur einmal verwendet werden können:
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 exhaustedUm erneut zu iterieren, müssen Sie eine neue Instanz erstellen:
# 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:
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, 3Dieses Muster trennt Zuständigkeiten:
CounterIterableist das Iterable – es weiß, wie man Iteratoren erstelltCounterIteratorist 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:
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 California35.4.7) Wann man benutzerdefinierte Iteratoren verwenden sollte
Erstellen Sie benutzerdefinierte Iteratoren, wenn:
- Sie Lazy Evaluation brauchen: Werte bei Bedarf erzeugen, statt sie alle zu speichern
- Sie eine benutzerdefinierte Datenstruktur haben: Machen Sie sie iterierbar, damit sie mit
for-Schleifen funktioniert - Sie spezielle Iterationslogik brauchen: Elemente überspringen, Werte transformieren oder komplexes schrittweises Durchlaufen implementieren
- 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.