3. Unterrichtsblock

Iterable und Iteratoren

Ein Iterator ist ein Objekt, das eine zählbare Anzahl von Werten enthält und das iteriert werden kann, was bedeutet, dass man alle Werte mit einer Schleife durchlaufen kann. Iteratoren werden oft auch inkrementiert, oder dekrementiert. Iteratoren dienen häufig auch als sog. Zähler innerhalb von Iterationen.

Eine Iterable ist alles, was mit einer Schleife durchlaufen werden kann (z. B. Strings, oder Arrays)

Hinweis: Jeder Iterator ist auch ein Iterable, aber nicht jede Iterable ist ein Iterator in Python.

Unterschied zwischen Iterator und Iterable

Listen, Tupel, Dictionarys und Sets sind allesamt iterierbare Objekte. Es handelt sich um iterierbare Container , aus denen man einen Iterator erhalten kann.

Alle diese Objekte verfügen über eine iter()-Methode, mit der ein Iterator abgerufen wird.

Hinweis: Jeder Iterator ist auch eine Iterable, aber nicht jede Iterable ist ein Iterator in Python.

Iterable ist ein Objekt, über das man iterieren kann. Es generiert einen Iterator, wenn es an die Methode iter() übergeben wird. Ein Iterator ist ein Objekt, das verwendet wird, um mit der Methode __next__() über ein iterierbares Objekt zu iterieren. Iteratoren verfügen über die Methode __next__(), die das nächste Element des Objekts zurückgibt.

Technisch gesehen ist ein Iterator in Python ein Objekt, das das Iteratorprotokoll implementiert, das aus den Methoden __iter__() und __next__() besteht.

Beispielsweise ist eine Liste iterierbar, aber eine Liste ist kein Iterator. Mit der Funktion iter() kann aus einem Iterable ein Iterator erstellt werden. Um dies zu ermöglichen, benötigt die Klasse eines Objekts entweder eine Methode __iter__, die einen Iterator zurückgibt, oder eine __getitem__-Methode mit sequentiellen Indizes beginnend mit 0.

Beispiel:

Wir wissen, dass str iterierbar ist, aber es ist kein Iterator. Wenn wir dies in einer for-Schleife ausführen, um eine Zeichenfolge zu drucken, ist dies möglich, da die for-Schleife bei der Ausführung in einen Iterator konvertiert wird, um den Code auszuführen.

Weitere Beispiele:

Eine ITERABLE ist:

  • alles, was in einer Schleife durchlaufen werden kann (d. h. man kann eine Schleife über eine Zeichenfolge oder eine Datei durchlaufen) oder
  • alles, was auf der rechten Seite einer for-Schleife erscheinen kann: for x in iterable: … oder
  • alles, was Sie mit iter() aufrufen können und das einen ITERATOR zurückgibt: iter(obj) oder
  • ein Objekt, das __iter__ definiert, das einen neuen ITERATOR zurückgibt, oder es verfügt möglicherweise über eine __getitem__-Methode, die für die indizierte Suche geeignet ist.

Ein ITERATOR ist ein Objekt:

  • mit Zustand, der sich während der Iteration merkt, wo er sich befindet,
  • mit einer __next__ Methode die:
    • gibt den nächsten Wert in der Iteration zurück
    • aktualisiert den Status so, dass er auf den nächsten Wert zeigt
    • signalisiert, wenn er fertig ist, indem eine StopIteration ausgelöst wird
  • und das ist selbstiterierbar (was bedeutet, dass es eine __iter__-Methode hat, die self zurückgibt).

Info:

  • die __next__-Methode in Python 3 wird in Python 2 als next geschrieben und
  • die integrierte Funktion next() ruft diese Methode für das ihr übergebene Objekt auf.

Beispiel:

>>> s = 'Katze'    # s ist eine ITERABLE
                   # s ist ein str Object das unveränderlich ist
                   # s hat keinen Zustand
                   # s hat die __getitem__() Methode 

>>> t = iter(s)    # t ist ein ITERATOR
                   # t hat einen Status (es beginnt damit, dass es auf das „K“ zeigt)
                   # t verfügt über eine next()-Methode und eine __iter__()-Methode

>>> next(t)        # Die Funktion next() gibt den nächsten Wert zurück und erhöht den Status
'K'
>>> next(t)        # Die Funktion next() gibt den nächsten Wert zurück und fährt fort
'a'
>>> next(t)        # Die Funktion next() gibt den nächsten Wert zurück und fährt fort
't'
>>> next(t)        # Die Funktion next() gibt den nächsten Wert zurück und fährt fort
'z'
>>> next(t)        # Die Funktion next() gibt den nächsten Wert zurück und fährt fort
'e'
>>> next(t)        # next() löst StopIteration aus, um zu signalisieren, dass die Iteration abgeschlossen ist
Traceback (most recent call last):
...
StopIteration

>>> iter(t) is t   # Der Iterator ist selbstiterierbar

Gibt einen Iterator aus einem Tupel zurück und gibt jeden Wert aus:

meintupel = ("Apfel", "Banane", "Kirsche")
meinit = iter(meintupel)

print(next(meinit))
print(next(meinit))
print(next(meinit))

Strings sind ebenfalls iterierbare Objekte, die eine Folge von Zeichen enthalten:

meinstr = "Banane"
meinit = iter(meinstr)

print(next(meinit))
print(next(meinit))
print(next(meinit))
print(next(meinit))
print(next(meinit))
print(next(meinit))

Durchlaufen eines Iterators

Wir können auch eine for-Schleife verwenden, um ein iterierbares Objekt zu durchlaufen. Iterieren der Werte eines Tupels:

meintupel = ("Apfel", "Banane", "Kirsche")

for x in meintupel:
  print(x)

Iterieren der Zeichen einer Zeichenfolge:

meinstr = "Banane"

for x in meinstr:
  print(x)

Die for-Schleife erstellt tatsächlich ein Iteratorobjekt und führt die next()-Methode für jede Schleife aus.

Erstellen eines Iterators

Um ein Objekt/eine Klasse als Iterator zu erstellen, muss man die Methoden __iter__()und __next__()für das Objekt implementieren.

Wie wie wir im 11. Unterrichtsblock erfahren haben , verfügen alle Klassen über eine Funktion namens __init__(), mit der man beim Erstellen des Objekts einige Initialisierungen durchführen kann.

Die __iter__()Methode verhält sich ähnlich, man können Operationen durchführen (Initialisierung etc.), muss aber immer das Iteratorobjekt selbst zurückgeben.

Mit der __next__()Methode kann man auch Operationen ausführen und erhält das nächste Element in der Sequenz zurück.

Erstellen eines Iterators, der Zahlen zurückgibt, beginnend mit 1, und jede Sequenz erhöht sich um eins (gibt 1,2,3,4,5 usw. zurück):

class MeineZahlen:
  def __iter__(self):
    self.a = 1
    return self

  def __next__(self):
    x = self.a
    self.a += 1
    return x

meineklasse = MeineZahlen()
meiniterator = iter(meineklasse)

print(next(meiniterator))
print(next(meiniterator))
print(next(meiniterator))
print(next(meiniterator))
print(next(meiniterator))

StopIteration Anweisung

Das obige Beispiel würde ewig weitergehen, wenn Sie genügend next()-Anweisungen hätten oder wenn es in einer Schleife verwendet würde for.

Um zu verhindern, dass die Iteration ewig dauert, können wir die StopIteration-Anweisung verwenden.

In der __next__()-Methode können wir eine Abbruchbedingung hinzufügen, um einen Fehler auszulösen, wenn die Iteration eine bestimmte Anzahl von Malen durchgeführt wird:

Stoppen nach 20 Iterationen:

class MeineZahlen:
  def __iter__(self):
    self.a = 1
    return self

  def __next__(self):
    if self.a <= 20:
      x = self.a
      self.a += 1
      return x
    else:
      raise StopIteration

meineklasse = MeineZahlen()
meineiteration = iter(meineklasse)

for x in meineiteration:
  print(x)

Aufgabe

  1. Iterieren über eine Liste: Erstelle eine Liste von Obstsorten und verwende eine Schleife, um jedes Element in der Liste auszugeben.
  2. Berechnung mit einem Iterator: Schreibe eine Funktion, die einen Iterator für die Fibonacci-Folge erstellt. Gib die ersten 10 Zahlen der Fibonacci-Folge aus.
  3. Iterieren über eine Zeichenkette: Gebe jeden Buchstaben eines beliebigen Wortes rückwärts aus, indem du eine Schleife und einen Iterator verwendest.
  4. Eigener Iterator: Erstelle eine Klasse, die einen Iterator für die Quadrate von Zahlen von 1 bis 10 erstellt. Verwende diese Klasse, um die Quadrate auszugeben.
  5. Filtern von Elementen: Gegeben ist eine Liste von Zahlen. Schreibe eine Funktion, die nur die geraden Zahlen aus der Liste ausgibt, indem du einen Iterator und eine bedingte Anweisung verwendest.

Fehlerbehandlung mit try & except

  • Mit dem try-Block kann man einen Codeblock auf Fehler testen.
  • Mit dem except-Block kann man den Fehler behandeln.
  • Mit dem else-Block kann man Code ausführen, wenn kein Fehler vorliegt.
  • Mit dem finally-Block kann man Code ausführen, unabhängig vom Ergebnis der Try- und Except-Blöcke.

Ausnahmebehandlung

Wenn ein Fehler oder eine Ausnahme, wie wir sie nennen, auftritt, stoppt Python normalerweise und generiert eine Fehlermeldung. Diese Ausnahmen können mit der try-Anweisung behandelt werden.

Der try-Block generiert eine Ausnahme, da x-nicht definiert ist:

try:
  print(x)
except:
  print("Eine Ausnahme trat auf")

Da der try-Block einen Fehler auslöst, wird der except-Block ausgeführt. Ohne den try-Block stürzt das Programm ab und gibt einen Fehler aus.

Diese Anweisung löst einen Fehler aus, da x nicht definiert ist:

print(x)

Viele Ausnahmen

Sie können beliebig viele Ausnahmeblöcke definieren, z. B. wenn Sie einen speziellen Codeblock für eine spezielle Fehlerart ausführen möchten.

Liste von Ausnahmen (Exceptions): https://docs.python.org/3/library/exceptions.html

Gibt eine Meldung aus, wenn der try-Block einen NameError Fehler auslöst und eine weitere Meldung für andere Fehler:

try:
  print(x)
except NameError:
  print("Variable x ist nicht definiert")
except:
  print("Etwas anderes lief falsch")

else

Mit dem Schlüsselwort else kann man einen Codeblock definieren, der ausgeführt werden soll, wenn keine Fehler aufgetreten sind.

In diesem Beispiel generiert der try Block keinen Fehler:

try:
  print("Hallo")
except:
  print("Etwas lief falsch")
else:
  print("Nichts lief falsch")

finally

Der finally-Block wird, sofern angegeben, unabhängig davon ausgeführt, ob der try-Block einen Fehler auslöst oder nicht.

try:
  print(x)
except:
  print("Etwas lief falsch")
finally:
  print("'try except' ist beendet")

Dies kann nützlich sein, um Objekte zu schließen und Ressourcen zu bereinigen.

Versuch, eine Datei zu öffnen und diese zu beschreiben, die nicht beschreibbar ist:

try:
  f = open("demodatei.txt")
  try:
    f.write("Lorem Ipsum")
  except:
    print("Etwas lief beim Beschreiben der Datei schief")
  finally:
    f.close()
except:
  print("Etwas lief beim Öffnen der Datei schief")

Das Programm kann fortgesetzt werden, ohne dass das Dateiobjekt geöffnet bleibt.

Auslösen einer Ausnahme

Als Python-Entwickler kann man eine Ausnahme auslösen, wenn eine Bedingung auftritt. Um eine Ausnahme auszulösen, verwendet man das raise-Schlüsselwort.

Auslösen eines Fehlers und Stoppen des Programms, wenn x kleiner als 0 ist:

x = -1

if x < 0:
  raise Exception("Entschuldigung, die Zahl ist kleiner als Null")

Das raise-Schlüsselwort wird hierbei verwendet, um eine Ausnahme auszulösen.

Man kann festlegen, welche Art von Fehler ausgegeben werden soll und welchen Text der Benutzer ausgeben möchte.

Löst einen TypeError aus, wenn x keine Ganzzahl ist:

x = "Servus"

if not type(x) is int:
  raise TypeError("Nur Ganzzahlen erlaubt")

Aufgabe

Baue eine Eingabe für einen Taschenrechner, die prüft, ob die eingegebene Zahl eine Ganzzahl ist.

  1. Teilung mit Ausnahmebehandlung: Schreibe ein Python-Programm, das den Benutzer nach zwei Zahlen fragt und versucht, die erste Zahl durch die zweite Zahl zu teilen. Verwende Try & Except, um Fehler abzufangen, falls die zweite Zahl 0 ist.
  2. Umwandlung in Integer mit Ausnahmebehandlung: Schreibe ein Programm, das den Benutzer nach einer Zeichenkette fragt und versucht, diese in eine Ganzzahl umzuwandeln. Verwende Try & Except, um Fehler abzufangen, falls die Eingabe keine gültige Ganzzahl ist.
  3. Zugriff auf Listenelement mit Ausnahmebehandlung: Erstelle eine Liste mit einigen Elementen. Frage den Benutzer nach einem Index und versuche dann, auf das Element an diesem Index zuzugreifen. Verwende Try & Except, um Fehler abzufangen, falls der angegebene Index außerhalb des gültigen Bereichs liegt.
  4. Division mit Ausnahmebehandlung für verschiedene Fehlerarten: Schreibe ein Programm, das den Benutzer nach zwei Zahlen fragt und versucht, die Division durchzuführen. Verwende Try & Except, um verschiedene Arten von Fehlern abzufangen, wie z.B. ValueError, wenn keine Zahl eingegeben wurde, oder ZeroDivisionError, wenn versucht wird, durch Null zu teilen.

String Formatierung

Um sicherzustellen, dass eine Zeichenfolge wie erwartet angezeigt wird, können wir das Ergebnis mit der format()-Methode formatieren.

String-Formatierung mit format()

Mit der format()Methode kann man ausgewählte Teile einer Zeichenfolge formatieren.

Manchmal gibt es Teile eines Textes, auf die man keinen Einfluss hat, weil diese vielleicht aus einer Datenbank stammen, oder durch eine Benutzereingabe hinzugefügt werden.

Um solche Werte zu steuern, fügt man Platzhalter (geschweifte Klammern {}) in den Text ein und führt die Werte durch die format()Methode aus:

Im folgenden Beispiel wird eine Platzhalter an der Stelle verwendet, an der man den Preis anzeigen lassen möchte:

preis = 49
txt = "Der Preis liegt bei {} Euro"
print(txt.format(preis))

Man kann innerhalb der geschweiften Klammern auch Parameter hinzufügen, um anzugeben, wie der Wert konvertiert werden soll. In folgendem Beispiel wird der anzuzeigenden Preis als Zahl mit zwei Dezimalstellen formatiert.

txt = "Der Preis liegt bei {:.2f} Euro"

Eine Sammlung von Formatierungen findet sich in folgender Referenz: https://learnpython.com/blog/python-string-formatting/

Mehrere Werte

Wenn man mehr Werte verwenden möchten, fügt man einfach weitere Werte zur format()-Methode hinzu:

print(txt.format(preis, produktnr, anzahl))

Weitere Platzhalter hinzufügen:

anzahl = 3
produktnr = 567
preis = 49
meineBestellung = "Ich möchte {} Stück der Produktnummer {} für {:.2f} Euro."
print(meineBestellung.format(anzahl, produktnr, preis))

Indexnummer

Man kann Indexnummern (eine Zahl in den geschweiften Klammern {0}) verwenden, um sicherzustellen, dass die Werte in den richtigen Platzhaltern platziert werden:

anzahl = 3
produktnr = 567
preis = 49
meineBestellung = "Ich möchte {0} Stück der Produktnummer {1} für {2:.2f} Euro."
print(meineBestellung.format(anzahl, produktnr, preis))

Wenn man mehr als einmal auf denselben Wert verweisen möchte, verwenden man ebenfalls die Indexnummer:

alter = 36
name = "Peter"
txt = "Sein Name ist {1}. {1} ist {0} Jahre alt."
print(txt.format(alter, name))

Benannte Indizes

Mann kann benannte Indizes auch verwenden, indem man einen Namen in die geschweiften Klammern eingibt z. B. {autoname}. Dann muss man jedoch auch Namen verwenden, wenn man die Parameterwerte übergeben möchte:


meineBestellung = "Ich habe einen {carname} {model}."
print(meineBestellung.format(autoname = "Ford", model = "Mustang")

Aufgabe

  1. Name und Alter: Schreibe ein Programm, das nach deinem Namen und Alter fragt und dann eine Nachricht ausgibt, die deinen Namen und dein Alter in einem Satz formatiert.
  2. Personalisierte Begrüßung: Fordere den Benutzer auf, seinen Vornamen einzugeben. Dann begrüße ihn mit einer personalisierten Nachricht, die seinen Vornamen in die Begrüßung einbezieht, z.B. „Hallo, Max! Wie geht es dir heute?“