Generatoren und Iteratoren¶
Einführung¶
<img class="imgright" width="300" height="235"src="images/wind_generators_posterize_300w.webp" alt="Wind Power Generators" />
Iteratoren liefern Elemente nacheinander über next(). Das Iteratorprotokoll erlaubt dadurch eine Verarbeitung, ohne dass alle Ergebnisse gleichzeitig als Liste im Speicher liegen müssen. Ob und wie viel Arbeit tatsächlich verzögert erfolgt, hängt vom konkreten Iterator ab.
Generatoren sind Pythons besonders kompakte Möglichkeit, Iteratoren zu erzeugen. Eine Generatorfunktion enthält yield; ihr Aufruf führt den Funktionskörper zunächst nicht aus, sondern erzeugt ein Generatorobjekt. Erst beim Iterieren bzw. bei next() läuft der Code bis zum nächsten yield. Der Zustand der lokalen Variablen bleibt zwischen diesen Unterbrechungen erhalten.
Das macht Generatoren besonders nützlich für große Datenströme und potenziell unendliche Folgen.
städte = ["Paris", "Berlin", "Hamburg",
"Frankfurt", "London", "Wien",
"Amsterdam", "Den Haag"]
for ort in städte:
print("ort: " + ort)
Bei einer for-Schleife wird zunächst iter(obj) aufgerufen. Ist das Objekt nicht iterierbar, entsteht ein TypeError. Andernfalls liefert iter() einen Iterator. Die Schleife ruft intern wiederholt next(iterator) auf und beendet sich, sobald der Iterator StopIteration signalisiert.
kenntnisstand = ["Anfänger", "Fortgeschritten", "Experte"]
kenntnisstand_iterator = iter(kenntnisstand)
print("Erster 'next'-Aufruf: ", next(kenntnisstand_iterator))
print("Zweiter'next'-Aufruf: ", next(kenntnisstand_iterator))
Nach dem letzten Element signalisiert der Iterator sein Ende durch StopIteration.
Wir können dieses Iterationsverhalten der for-Schleife in einer while-Schleife simulieren: Möglicherweise haben Sie bemerkt, dass in unserem Programm etwas fehlt: Wir müssen die Ausnahme "StopIteration" abfangen:
andere_städte = ["Strasbourg", "Freiburg", "Stuttgart", "Wien", "Hannover", "Berlin", "Zürich"]
stadt_iterator = iter(andere_städte)
while True:
try:
stadt = next(stadt_iterator)
except StopIteration:
break
print(stadt)
Die sequentiellen Basistypen sowie die Mehrheit der Klassen der Standardbibliothek von Python unterstützen die Iteration. Der Wörterbuch-Datentyp dict unterstützt auch Iteratoren. In diesem Fall läuft die Iteration über die Schlüssel des Wörterbuchs:
hauptstädte = {"Frankreich":"Paris",
"Niederlande":"Amsterdam",
"Deutschland":"Berlin",
"Schweiz":"Bern",
"Österreich":"Wien"}
for land in hauptstädte:
print("Die Hauptstadt von " + land + " ist " + hauptstädte[land])
Off-Topic: Einige Leser sind möglicherweise verwirrt, wenn sie aus unserem Beispiel erfahren, dass die Hauptstadt der Niederlande nicht Den Haag (Den Haag), sondern Amsterdam ist. Amsterdam ist laut Verfassung die Hauptstadt der Niederlande, obwohl sich das niederländische Parlament und die niederländische Regierung in Den Haag befinden, ebenso wie der Oberste Gerichtshof und der Staatsrat.
Einen Iterator als Klasse implementieren¶
Eigene Iterator-Klassen sind sinnvoll, wenn der Iterationszustand als Objekt mit zusätzlichem Verhalten modelliert werden soll. Die Klasse implementiert __iter__() und __next__().
Das folgende Beispiel wiederholt die Elemente eines nichtleeren Iterables zyklisch. Für ein leeres Iterable beendet der Iterator korrekt mit StopIteration:
class Zyklus:
def __init__(self, iterable):
self.elemente = tuple(iterable)
self.index = 0
def __iter__(self):
return self
def __next__(self):
if not self.elemente:
raise StopIteration
wert = self.elemente[self.index]
self.index = (self.index + 1) % len(self.elemente)
return wert
x = Zyklus("abc")
for _ in range(10):
print(next(x), end=", ")
Eine Iterator-Klasse ist keineswegs „unpythonisch“. Wenn lediglich eine Folge von Werten erzeugt werden soll, ist eine Generatorfunktion jedoch oft kürzer und leichter lesbar.
Generatoren¶
Eine Funktion, deren Körper yield enthält, ist eine Generatorfunktion. Beim Aufruf entsteht ein Generatorobjekt, das zugleich Iterator ist.
Bei next(generator) läuft die Funktion bis zum nächsten yield. Der dort angegebene Wert wird an den Aufrufer geliefert und die Ausführung pausiert. Beim nächsten next() wird direkt nach diesem yield fortgesetzt; lokale Variablen und der Ausführungszustand bleiben erhalten.
Erreicht die Generatorfunktion ihr Ende oder führt sie return aus, ist der Iterator erschöpft und signalisiert StopIteration.
def stadt_generator():
yield "Hamburg"
yield "Konstanz"
yield "Berlin"
yield "Zürich"
yield "Schaffhausen"
yield "Stuttgart"
Wir haben einen Iterator erstellt, indem wir stadt_generator() aufgerufen haben:
stadt = stadt_generator()
print(next(stadt))
print(next(stadt))
print(next(stadt))
print(next(stadt))
print(next(stadt))
print(next(stadt))
print(next(stadt))
Jeder next(stadt)-Aufruf liefert die nächste Stadt. Nach dem letzten yield ist der Generator erschöpft; ein weiterer next()-Aufruf löst StopIteration aus.
Ein erschöpftes Generatorobjekt lässt sich nicht „zurücksetzen“. Stattdessen erzeugt man durch einen erneuten Funktionsaufruf ein neues Generatorobjekt, beispielsweise stadt = stadt_generator().
Eine return-Anweisung beendet eine normale Funktion sofort. In einer Generatorfunktion beendet return die Iteration; ein optionaler Rückgabewert wird dabei als Wert der intern erzeugten StopIteration transportiert.
def zahl(firstval=0, step=1):
x = firstval
while True:
yield x
x += step
zähler = zahl() # count beginnt mit 0
for i in range(10):
print(next(zähler), end=", ")
start_wert = 2.1
stop_wert = 0.3
print("\nNeuer Zähler:")
zähler = zahl(start_wert, stop_wert)
for i in range(10):
neue_wert = next(zähler)
print(f"{neue_wert:2.2f}", end=", ")
Fibonacci-Sequenz als Generator:
Die Fibonacci-Sequenz ist nach Leonardo von Pisa benannt, der als Fibonacci bekannt war (eine Kontraktion von Filius Bonacci, "Sohn von Bonaccio"). In seinem Lehrbuch Liber Abaci, das im Jahr 1202 erschien, hatte er eine Aufgabe, die sich mit Kaninchen und ihre Fortpflanzung: Es geht los einem neugeborenen Kaninchenpaar, d. h. mit einem Männchen und einem Weibchen. Es dauert einen Monat, bis sie sich paaren können. Am Ende des zweiten Monats bringt das Weibchen ein neues Kaninchenpaar zur Welt. Nehmen wir nun an, dass jedes weibliche Kaninchen jeden Monat nach dem Ende des ersten Monats ein weiteres Kaninchenpaar zur Welt bringt. Wir müssen erwähnen, dass Fibonaccis Kaninchen niemals sterben. Nun kann man sich fragen, wie groß die Population nach einer bestimmten Anzahl von Monaten sein wird.
Dies erzeugt eine Folge von Zahlen: 0, 1, 1, 2, 3, 5, 8, 13
Diese Sequenz kann in mathematischen Begriffen wie folgt definiert werden:
$F_n = F_{n - 1} + F_{n - 2}$ mit den Startwerten: $F_0 = 0$ and $F_1 = 1$
def fibonacci(n):
""" Ein Generator zum Erstellen der Fibonacci-Zahlen"""
a, b, zähler = 0, 1, 0
while True:
if (zähler > n):
return
yield a
a, b = b, a + b
zähler += 1
f = fibonacci(5)
for x in f:
print(x, " ", end="") #
print()
Der obige Generator kann verwendet werden, um die ersten n durch Leerzeichen getrennten Fibonacci-Zahlen oder bessere (n + 1) Zahlen zu erstellen, da auch die 0. Zahl enthalten ist. Im nächsten Beispiel stellen wir eine Version vor, die einen endlosen Iterator zurückgeben kann. Wir müssen bei der Verwendung dieses Iterators darauf achten, dass ein Beendigungskriterium verwendet wird:
def fibonacci():
"""Erzeugt bei Bedarf eine unendliche Folge von Fibonacci-Zahlen"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
f = fibonacci()
zähler = 0
for x in f:
print(x, " ", end="")
zähler += 1
if (zähler > 10):
break
print()
return und StopIteration in Generatoren¶
Eine Generatorfunktion darf return verwenden. return wert beendet den Generator; beim manuellen Aufruf von next() kann der Wert über StopIteration.value beobachtet werden.
Wichtig: StopIteration sollte innerhalb einer Generatorfunktion nicht selbst ausgelöst werden. Ein direkt oder indirekt aus dem Generatorcode entweichendes StopIteration wird von Python in RuntimeError umgewandelt. Das verhindert schwer erkennbare vorzeitige Abbrüche.
def falsch():
yield 1
raise StopIteration(42) # nicht so beenden!
g = falsch()
print(next(g))
try:
next(g)
except RuntimeError as exc:
print(type(exc).__name__, exc)
Korrekt beendet man einen Generator mit return:
def gen():
yield 1
return 42
g = gen()
print(next(g))
try:
next(g)
except StopIteration as exc:
print("Rückgabewert:", exc.value)
send() und Generatoren als einfache Coroutinen¶
Ein Generator kann nicht nur Werte mit yield liefern, sondern über send() auch einen Wert empfangen. Ein Ausdruck wie x = yield wert liefert zunächst wert; beim Fortsetzen erhält x den an send() übergebenen Wert.
Ein neu erzeugter Generator muss zunächst bis zum ersten yield vorgespult werden. Klassisch geschieht das mit next(generator). next(generator) ist für einen bereits gestarteten Generator hinsichtlich des hineingesendeten Werts äquivalent zu generator.send(None).
def einfache_koroutine():
print("Koroutine wurde gestartet!")
while True:
x = yield "foo"
print("Koroutine empfing diesen Wert: ", x)
cr = einfache_koroutine()
cr
next(cr)
ret_wert = cr.send("Hi")
print("'send' gibt zurück: ", ret_wert)
Wir mussten zuerst den Generator einmal mittels next starten, weil der Generator gestartet werden muss. Die Verwendung von send an einen Generator, der noch nicht gestartet wurde, führt zu einer Ausnahme.
Damit ein gesendeter Wert im Generator verwendet werden kann, steht yield typischerweise in einem Ausdruck wie neu = yield aktueller_wert. Nach dem Fortsetzen enthält neu den von send() übertragenen Wert; bei next() ist dieser Wert None.
def zähle(erstewert=0, schritt=1):
zähler = erstewert
while True:
neu_zähler_wert = yield zähler
if neu_zähler_wert is None:
zähler += schritt
else:
zähler = neu_zähler_wert
start_wert = 2.1
stop_wert = 0.3
zähler = zähle(start_wert, stop_wert)
for i in range(10):
neu_wert = next(zähler)
print(f"{neu_wert:2.2f}", end=", ")
print()
print("Setzen Sie den aktuellen Zählwert auf einen anderen Wert:")
zähler.send(100.5)
for i in range(10):
neu_wert = next(zähler)
print(f"{neu_wert:2.2f}", end=", ")
Die throw()-Methode¶
Mit generator.throw(exc) wird an der aktuell pausierten Stelle eine Exception in den Generator hineingeworfen. Fängt der Generator diesen Exception-Typ ab, kann er weiterlaufen und erneut einen Wert mit yield liefern. Andernfalls propagiert die Exception zum Aufrufer.
Für normale Datenübertragung ist send() meist klarer; throw() ist vor allem nützlich, wenn der Generator gezielt auf außergewöhnliche Zustände reagieren soll.
def zähle(erstewert=0, schritt=1):
zähler = erstewert
while True:
try:
neu_zähler_wert = yield zähler
if neu_zähler_wert is None:
zähler += schritt
else:
zähler = neu_zähler_wert
except Exception:
yield (erstewert, schritt, zähler)
Im folgenden Codeblock zeigen wir, wie dieser Generator verwendet werden kann:
c = zähle()
for i in range(3):
print(next(c))
print("Schauen wir uns den Zustand des Iterators an:")
i = c.throw(Exception)
print(i)
print("Nun können wir fortfahren:")
for i in range(3):
print(next(c))
Wir können das vorherige Beispiel verbessern, indem wir unsere eigene Ausnahmeklasse StateOfGenerator definieren:
class GeneratorStatus(Exception):
def __init__(self, nachricht=None):
self.nachricht = nachricht
def zahl(erstewert=0, schritt=1):
zähler = erstewert
while True:
try:
neu_zähler_wert = yield zähler
if neu_zähler_wert is None:
zähler += schritt
else:
zähler = neu_zähler_wert
except GeneratorStatus:
yield (erstewert, schritt, zähler)
Wir können den vorherigen Generator folgendermaßen verwenden:
c = zahl()
for i in range(3):
print(next(c))
print("Lassen Sie uns sehen, wie der Zustand des Iterators ist:")
i = c.throw(GeneratorStatus("hi"))
print(i)
print("Jetzt können wir fortfahren:")
for i in range(3):
print(next(c))
yield from¶
yield from iterable delegiert einen Teil der Iteration an ein anderes Iterable bzw. einen Subgenerator. Für einfache Iterables ersetzt es häufig eine Schleife der Form for x in iterable: yield x.
Bei einem Subgenerator delegiert yield from zusätzlich send(), throw() und close() entsprechend dem Generatorprotokoll. Beendet der Subgenerator sich mit return wert, wird dieser Wert zum Ergebnis des yield from-Ausdrucks.
def gen1():
for char in "Python":
yield char
for i in range(5):
yield i
def gen2():
yield from "Python"
yield from range(5)
g1 = gen1()
g2 = gen2()
print("g1: ", end=", ")
for x in g1:
print(x, end=", ")
print("\ng2: ", end=", ")
for x in g2:
print(x, end=", ")
print()
Wir können an der Ausgabe erkennen, dass beide Generatoren, was das Verhalten und die Ergebnisse betrifft, gleich sind.
Der Vorteil einer yield from-Anweisung kann als eine Möglichkeit angesehen werden, einen Generator in mehrere Generatoren aufzuteilen. Das haben wir in unserem vorherigen Beispiel getan, und wir werden dies im folgenden Beispiel deutlicher demonstrieren:
def städte():
for stadt in ["Berlin", "Hamburg", "München", "Freiburg"]:
yield stadt
def quadrat():
for nummer in range(10):
yield nummer ** 2
def generator_all_in_one():
for stadt in städte():
yield stadt
for nummer in quadrat():
yield nummer
def generator_splitted():
yield from städte()
yield from quadrat()
lst1 = [el for el in generator_all_in_one()]
lst2 = [el for el in generator_splitted()]
print(lst1 == lst2)
Der vorherige Code gibt True zurück, da die Generatoren generator_all_in_one und generator_splitted dieselben Elemente liefern.
Dies bedeutet, dass, wenn der <expr> von yield from ein anderer Generator ist, der Effekt derselbe ist, als wäre der Körper des Subgenerators an der Stelle der yield from-Anweisung eingefügt worden. Darüber hinaus darf der Subgenerator eine return-Anweisung mit einem Wert ausführen, und der Wert wird der Wert von yield from. Wir zeigen dies mit dem folgenden kleinen Skript:
def subgenerator():
yield 1
return 42
def delegierender_generator():
x = yield from subgenerator()
print(x)
for x in delegierender_generator():
print(x)
Rekursive Generatoren¶
Das folgende Beispiel ist ein Generator zum Erstellen aller Permutationen einer bestimmten Liste von Elementen.
Für diejenigen, die nicht wissen, was Permutationen sind, haben wir eine kurze Einführung:
Formale Definition:
Das Begriff Permutation kommt vom Lateinischen "permutare", was im Deutschen "vertauschen" bedeutet.
Eine Permutation ist eine Vertauschung bzw. Neuanordnung der Elemente einer geordneten Liste. Mit anderen Worten: Jede Anordnung von n Elementen wird als Permutation bezeichnet.
In den folgenden Zeilen zeigen wir alle Permutationen der Buchstaben a, b und c:
a b c
a c b
b a c
b c a
c a b
c b a
Die Anzahl der Permutationen auf einer Menge von n Elementen ist gegeben durch n!
n! = n * (n-1) * (n-2) ... 2 * 1
n! heißt die Fakultät von n.
Der Permutationsgenerator kann mit einer beliebigen Liste von Objekten aufgerufen werden. Der von diesem Generator zurückgegebene Iterator generiert alle möglichen Permutationen:
def permutationen(items):
n = len(items)
if n==0: yield []
else:
for i in range(len(items)):
for cc in permutationen(items[:i]+items[i+1:]):
yield [items[i]]+cc
for p in permutationen(['r','e','d']): print(''.join(p))
for p in permutationen(list("game")): print(''.join(p) + ", ", end="")
Das vorherige Beispiel kann für Neulinge schwer zu verstehen sein. Wie immer bietet Python eine bequeme Lösung. Zu diesem Zweck benötigen wir das Modul itertools. Itertools ist ein sehr praktisches Tool zum Erstellen und Bearbeiten von Iteratoren.
Erstellen von Permutationen mit itertools:
import itertools
perms = itertools.permutations(['r','e','d'])
list(perms)
Der Begriff "Permutationen" kann manchmal in einer schwächeren Bedeutung verwendet werden. Permutationen können in dieser schwächeren Bedeutung eine Folge von Elementen bezeichnen, wobei jedes Element nur einmal vorkommt, ohne dass jedoch alle Elemente einer bestimmten Menge enthalten sein müssen. In diesem Sinne ist (1, 3, 5, 2) eine Permutation der Ziffernmenge {1, 2, 3, 4, 5, 6}. Wir können zum Beispiel alle Sequenzen einer festen Länge k von Elementen bauen, die aus einer gegebenen Menge der Größe n mit k ≤ n entnommen sind.
Dies sind alle 3-Permutationen der Menge {"a", "b", "c", "d"}.
Wählt man aus einer n-elementigen Menge von Objekten k Objekte unter Berücksichtigung der Reihenfolge ohne Zurücklegen aus, so bezeichnet man dies als eine Variation ohne Wiederholung. Die Permutationen ergeben sich als ein Sonderfall für n = k.
Die Anzahl solcher k-Permutationen von n wird mit $P_{n,k}$ bezeichnet und ihr Wert wird durch das Produkt berechnet:
$n · (n - 1) · … (n - k + 1)$
Unter Verwendung der Fakultätsnotation kann der oben erwähnte Ausdruck wie folgt geschrieben werden:
$P_{n, k} = n! / (n - k)!$
Ein Generator zur Erzeugung von k-Variationen von n Objekten sieht unserem vorherigen Permutationsgenerator sehr ähnlich:
def k_variations(items, n):
if n==0:
yield []
else:
for item in items:
for kp in k_variations(items, n-1):
if item not in kp:
yield [item] + kp
for kp in k_variations("abcd", 3):
print(kp)
Ein Generator von Generatoren¶
Der zweite Generator unseres Fibonacci-Sequenzbeispiels erzeugt einen Iterator, der theoretisch alle Fibonacci-Zahlen erzeugen kann, d. h. unendlich viele. Man sollte jedoch nicht versuchen, alle diese Zahlen in einer Liste mit der folgenden Zeile zu erstellen.
list(fibonacci())
Bei einem unendlichen Generator würde list(...) niemals regulär fertig werden und dabei immer weiter Speicher belegen.
In den meisten praktischen Anwendungen benötigen wir nur die ersten n Elemente. Wir können einen anderen Generator verwenden, in unserem Beispiel firstn, um die ersten n Elemente eines Generators generator zu erstellen:
def firstn(generator, n):
g = generator()
for i in range(n):
yield next(g)
Das folgende Skript gibt die ersten 10 Elemente der Fibonacci-Sequenz zurück:
def fibonacci():
""" Ein Fibonacci-Zahlengenerator """
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(firstn(fibonacci, 10)))
Aufgaben¶
Aufgabe 1¶
Schreibe einen Generator, der den laufenden Durchschnitt berechnet.
Aufgabe 2¶
Schreibe einen Generator frange, der sich wie range verhält, aber float Werte akzeptiert.
Aufgabe 3¶
Schreibe einen Generator trange, der eine Folge von Zeittupeln von Start bis Stopp erzeugt, die schrittweise erhöht werden. Ein Zeittupel ist ein 3-Tupel von ganzen Zahlen: (Stunden, Minuten, Sekunden)
Ein Aufruf von trange könnte also so aussehen:
trange ((10, 10, 10), (13, 50, 15), (0, 15, 12))
Aufgabe 4¶
Schreibe eine Version "rtrange" des vorherigen Generators, die Nachrichten empfangen kann, um den Startwert zurückzusetzen.
Aufgabe 5¶
Schreibe ein Programm mit dem neu geschriebenen Generator "trange", um eine Datei "times_and_temperatures.txt" zu erstellen. Die Zeilen dieser Datei enthalten eine Zeit im Format hh::mm::ss und zufällige Temperaturen zwischen 10,0 und 25,0 Grad. Die Zeiten sollten in Schritten von 90 Sekunden ab 6:00:00 Uhr ansteigen.
Beispielsweise:
06:00:00 20.1
06:01:30 16.1
06:03:00 16.9
06:04:30 13.4
06:06:00 23.7
06:07:30 23.6
06:09:00 17.5
06:10:30 11.0

Aufgabe 6¶
Schreibe einen Generator mit dem Namen random_ones_and_zeroes, der in jeder Iteration einen Bitstrom zurückgibt, d. h. jeweils eine Null oder eine Eins. Die Wahrscheinlichkeit p für die Rückgabe einer 1 ist in einer Variablen p definiert. Der Generator initialisiert diesen Wert auf 0.5. Mit anderen Worten, Nullen und Einsen werden mit der gleichen Wahrscheinlichkeit zurückgegeben.
Aufgabe 7¶
Wir haben eine Klasse Zyklus am Anfang dieses Kapitels unseres Python-Tutorials geschrieben. Schreibe nun einen Generator namens zyklus, der dieselbe Aufgabe erfüllt.
Lösungen für unsere Aufgaben¶
Lösung zur Aufgabe 1¶
def laufender_durchschnitt():
total = 0.0
zähler = 0
durchschnitt = None
while True:
term = yield durchschnitt
total += term
zähler += 1
durchschnitt = total / zähler
ra = laufender_durchschnitt() # Initialisieren Sie die Coroutine
next(ra) # Wir müssen die Coroutine starten
for wert in [7, 13, 17, 231, 12, 8, 3]:
out_str = "gesendet: {wert:3d}, laufender Durchschnitt: {drc:6.2f}"
print(out_str.format(wert=wert, drc=ra.send(wert)))
Lösung zur Aufgabe 2¶
def frange(*args):
startwert = 0
schrittgrösse = 1
if len(args) == 1:
endwert = args[0]
elif len(args) == 2:
startwert, endwert = args
elif len(args) == 3:
startwert, endwert, schrittgrösse = args
wert = startwert
while wert < endwert:
yield wert
wert += schrittgrösse
Die Verwendung von frange kann folgendermaßen aussehen:
for i in frange(5.6):
print(i, end=", ")
print()
for i in frange(0.3, 5.6):
print(i, end=", ")
print()
for i in frange(0.3, 5.6, 0.8):
print(i, end=", ")
print()
Lösung zur Aufgabe 3¶
%%writefile timerange.py
def trange(start, stopp, schrittweite):
"""
trange (stopp) -> Zeit als 3-Tupel (Stunden, Minuten, Sekunden)
trange (start, stopp [, schrittweite]) -> Zeittupel
start: Zeittupel (Stunden, Minuten, Sekunden)
stopp: Zeittupel
schrittweite: Zeittupel
Gibt eine Folge von Zeittupeln von `start` bis `stopp` zurück,
die mittels `schrittweise` erhöht werden
"""
aktuell = list(start)
while aktuell < list(stopp):
yield tuple(aktuell)
sekunde = schrittweite[2] + aktuell[2]
minuten_übertrag = 0
stunden_übertrag = 0
if sekunde < 60:
aktuell[2] = sekunde
else:
aktuell[2] = sekunde - 60
minuten_übertrag = 1
minuten = schrittweite[1] + aktuell[1] + minuten_übertrag
if minuten < 60:
aktuell[1] = minuten
else:
aktuell[1] = minuten - 60
stunden_übertrag = 1
stunden = schrittweite[0] + aktuell[0] + stunden_übertrag
if stunden < 24:
aktuell[0] = stunden
else:
aktuell[0] = stunden - 24
from timerange import trange
for time in trange((10, 10, 10), (19, 53, 15), (1, 24, 12) ):
print(time)
Lösung zur Aufgabe 4¶
%%writefile rtimerange.py
def rtrange(start, stopp, schrittweite):
"""
trange (stopp) -> Zeit als 3-Tupel (Stunden, Minuten, Sekunden)
trange (start, stopp [, schrittweite]) -> Zeittupel
start: Zeittupel (Stunden, Minuten, Sekunden)
stopp: Zeittupel
schrittweite: Zeittupel
Gibt eine Folge von Zeittupeln von `start` bis `stopp` zurück,
die mittels `schrittweise` erhöht werden
Der Generator kann durch Senden eines neuen "Start"-Werts resettiert werden.
"""
aktuell = list(start)
while aktuell < list(stopp):
neu_start = yield tuple(aktuell)
if neu_start is not None:
aktuell = list(neu_start)
continue
sekunde = schrittweite[2] + aktuell[2]
minuten_übertrag = 0
stunden_übertrag = 0
if sekunde < 60:
aktuell[2] = sekunde
else:
aktuell[2] = sekunde - 60
minuten_übertrag = 1
minuten = schrittweite[1] + aktuell[1] + minuten_übertrag
if minuten < 60:
aktuell[1] = minuten
else:
aktuell[1] = minuten - 60
stunden_übertrag = 1
stunden = schrittweite[0] + aktuell[0] + stunden_übertrag
if stunden < 24:
aktuell[0] = stunden
else:
aktuell[0] = stunden - 24
from rtimerange import rtrange
ts = rtrange((10, 10, 10), (17, 50, 15), (1, 15, 12) )
for _ in range(3):
print(next(ts))
print(ts.send((8, 5, 50)))
for _ in range(3):
print(next(ts))
Lösung zur Aufgabe 5¶
from timerange import trange
import random
with open("times_and_temperatures.txt", "w", encoding="utf-8") as fh:
for zeit in trange((6, 0, 0), (23, 0, 0), (0, 1, 30)):
zufallszahl = random.randint(100, 250) / 10
lst = zeit + (zufallszahl,)
ausgabe = "{:02d}:{:02d}:{:02d} {:4.1f}\n".format(*lst)
fh.write(ausgabe)
Weitere Details und den mathematischen Hintergrund zu dieser Übung finden Sie in unserem Kapitel über Gewichtete Wahrscheinlichkeiten.
Lösung zur Aufgabe 6¶
import random
def zufalls_einsen_und_nullen():
p = 0.5
while True:
x = random.random()
nachricht = yield 1 if x < p else 0
if nachricht is not None:
p = nachricht
x = zufalls_einsen_und_nullen()
next(x) # Der Rückgabewert interessiert uns nicht
for p in [0.2, 0.8]:
print("\nWir ändern die Wahrscheinlichkeit auf : " + str(p))
x.send(p)
for i in range(20):
print(next(x), end=" ")
print()
Lösung zur Aufgabe 7¶
Der "cycle"-Generator ist Teil des Moduls 'itertools'. Der folgende Code ist die Implementierung in itertools:
def cycle(iterable):
# cycle('ABCD') --> A B C D A B C D A B C D ...
saved = []
for element in iterable:
yield element
saved.append(element)
while saved:
for element in saved:
yield element
länder = ["Germany", "Switzerland", "Austria"]
länder_iterator = cycle(länder)
for i in range(7):
print(next(länder_iterator))

