Nächstes Kapitel: Anonyme Funktionen: Lambda-Operator, map- filter- und reduce
Generatoren und Iteratoren¶
Einführung¶
Was ist ein Iterator? Iteratoren sind Objekte, über die mir einer for-Schleife iteriert werden kann. Wir können auch sagen, dass ein Iterator ein Objekt ist, das Daten Element für Element zurückgibt. Das heißt, sie machen keine Arbeit, bis wir ausdrücklich nach ihrem nächsten Objekt fragen bzw. dieses anfordern. Sie arbeiten nach einem Prinzip, das in der Informatik als Lazy-Evaluation (deutsch: bequeme oder faule Auswertung) bekannt ist. Lazy-Evaluation ist eine Bewertungsstrategie, die die Bewertung eines Ausdrucks verzögert, bis sein Wert wirklich benötigt wird. Aufgrund der "Lazyness" von Python-Iteratoren sind sie eine großartige Möglichkeit, mit Unendlichkeit umzugehen, d. h. Iteratoren, die für immer iterieren können. Man findet kaum Python-Programme, die keine Iteratoren benutzen.
Iteratoren sind ein grundlegendes Konzept von Python. Sie haben bereits in Ihren ersten Python-Programmen gelernt, dass Sie Containerobjekte wie Listen und Zeichenfolgen durchlaufen können. Zu diesem Zweck erstellt Python eine Iteratorversion der Liste oder Zeichenfolge. In diesem Fall kann ein Iterator als Zeiger auf einen Container angesehen werden, sodass wir alle Elemente dieses Containers durchlaufen können. Ein Iterator ist eine Abstraktion, mit der Programmierende auf alle Elemente eines iterierbaren Objekts (eine Menge, eine Zeichenfolge, eine Liste usw.) zugreifen können, ohne die Datenstruktur dieses Objekts genauer zu kennen.
Generatoren sind eine spezielle Art von Funktion, mit der wir Iteratoren implementieren oder generieren können.
Meistens werden Iteratoren implizit verwendet, wie in der for-Schleife von Python. Wir zeigen dies im folgenden Beispiel. Wir iterieren über eine Liste, aber man sollte sich nicht irren: Eine Liste ist kein Iterator, kann aber wie ein Iterator verwendet werden:
städte = ["Paris", "Berlin", "Hamburg",
"Frankfurt", "London", "Wien",
"Amsterdam", "Den Haag"]
for ort in städte:
print("ort: " + ort)
Wenn eine for-Schleife ausgeführt wird passiert Folgendes: Die Funktion 'iter' wird auf das Objekt angewendet, das dem Schlüsselwort 'in' folgt, z. B. das o
in dem Ausdruck for i in o
: Zwei Fälle sind möglich: o
ist entweder iterierbar oder nicht. Wenn o
nicht iterierbar ist, wird eine Ausnahme ausgelöst, die besagt, dass der Typ des Objekts nicht iterierbar ist. Wenn o
dagegen iterierbar ist, gibt der Aufruf iter(o)
einen Iterator zurück. Nennen wir diesen iterator_obj
. Die for-Schleife verwendet diesen Iterator, um mit der next
-Methode über das Objekt o
zu iterieren. Die for-Schleife stoppt, wenn next(iterator_obj)
'exhausted' (erschöpft) ist, was bedeutet, dass eine StopIteration-Ausnahme zurückgegeben wird. Wir demonstrieren dieses Verhalten im folgenden Codebeispiel:
kenntnisstand = ["Anfänger", "Fortgeschritten", "Experte"]
kenntnisstand_iterator = iter(kenntnisstand)
print("Erster 'next'-Aufruf: ", next(kenntnisstand_iterator))
print("Zweiter'next'-Aufruf: ", next(kenntnisstand_iterator))
Wir hätten noch zweimal next
aufrufen können, aber danach erhalten wir eine StopIteration-Ausnahme.
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 "Stop Iteration" abfangen:
andere_städte = ["Strasbourg", "Freiburg",
"Stuttgart", "Wien",
"Hannover", "Berlin",
"Zurich"]
stadt_iterator = iter(andere_städte)
while stadt_iterator:
try:
stadt = next(stadt_iterator)
print(stadt)
except StopIteration:
break
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.
Iterator implementieren¶
Eine Möglichkeit, Iteratoren in Python zu erstellen, besteht darin, eine Klasse zu definieren, die die Methoden __init__
und __next__
implementiert. Wir zeigen dies, indem wir einen Klassenzyklus implementieren, mit dem ein iterierbares Objekt für immer durchlaufen werden kann. Mit anderen Worten, eine Instanz dieser Klasse gibt das Element einer Iterable zurück, bis es erschöpft ist. Dann wird die Sequenz auf unbestimmte Zeit wiederholt.
class Zyklus(object):
def __init__(self, iterable):
self.iterable = iterable
self.iter_obj = iter(iterable)
def __iter__(self):
return self
def __next__(self):
while True:
try:
next_obj = next(self.iter_obj)
return next_obj
except StopIteration:
self.iter_obj = iter(self.iterable)
x = Zyklus("abc")
for i in range(10):
print(next(x), end=", ")
Auch wenn der objektorientierte Ansatz zum Erstellen eines Iterators sehr interessant sein mag, ist dies nicht die pythonische Methode.
Der übliche und einfachste Weg, einen Iterator in Python zu erstellen, besteht in der Verwendung einer Generatorfunktion. Wie dies funktioniert zeigen wir um Folgenden.
Generatoren¶
An der Oberfläche sehen Generatoren in Python wie Funktionen aus, aber es gibt sowohl einen syntaktischen als auch einen semantischen Unterschied. Ein Unterscheidungsmerkmal sind die yield
-Anweisungen. Die yield
-Anweisung verwandelt eine Funktiondefinition in einen Generator. Ein Generator ist eine Funktion, die ein Generatorobjekt zurückgibt. Dieses Generatorobjekt kann als eine Funktion angesehen werden, die anstelle eines einzelnen Objekts eine Folge von Ergebnissen erzeugt. Diese Folge von Werten wird durch Iteration darüber erzeugt, z. B. mit einer for-Schleife. Die Werte, auf denen iteriert werden kann, werden mithilfe der yield
-Anweisung erstellt. Der von der yield
-Anweisung erstellte Wert ist der Wert, der auf das yield
-Schlüsselwort folgt. Die Ausführung des Codes wird beendet, wenn eine yield
-Anweisung erreicht ist. Der Wert des Ausdrucks, der hinter yield
folgt, wird zurückgegeben. Die Ausführung des Generators wird jetzt unterbrochen. Sobald next
erneut für das Generatorobjekt aufgerufen wird, nimmt die Generatorfunktion die Ausführung direkt nach der yield
-Anweisung im Code wieder auf, in dem der letzte Aufruf erfolgt. Die Ausführung wird in dem Zustand fortgesetzt, in dem der Generator nach letzten yield
verlassen wurde. Mit anderen Worten, alle lokalen Variablen sind noch vorhanden, da sie zwischen den Aufrufen automatisch gespeichert werden. Dies ist ein grundlegender Unterschied zu Funktionen: Funktionen beginnen ihre Ausführung immer am Anfang des Funktionskörpers, unabhängig davon, wo sie in früheren Aufrufen verlassen worden sind. Sie haben keine statischen oder dauerhaften Werte. Der Code eines Generators kann mehr als eine yield
-Anweisung enthalten, oder die yield
-Anweisung befindet sich möglicherweise im Hauptteil einer Schleife. Wenn der Code eines Generators eine return-Anweisung enthält, wird die Ausführung mit einem StopIteration-Ausnahmefehler beendet, wenn dieser Code vom Python-Interpreter ausgeführt wird. Das Wort "Generator" wird manchmal mehrdeutig verwendet, um sowohl die Generatorfunktion selbst als auch die Objekte zu bezeichnen, die von einem Generator erzeugt ("generiert") werden.
Alles, was mit einem Generator gemacht werden kann, kann auch mit einem klassenbasierten Iterator implementiert werden. Der entscheidende Vorteil von Generatoren besteht jedoch darin, dass die Methoden __iter__()
und next()
automatisch erstellt werden.
Generatoren bieten eine geeignete Möglichkeit, Daten zu erzeugen, die riesig oder sogar unendlich sind.
Das Folgende ist ein einfaches Beispiel für einen Generator, der verschiedene Städtenamen erzeugen kann.
Mit diesem Generator kann ein Generatorobjekt erstellt werden, das nacheinander die Städtenamen der yield
-Anweisungen generiert.
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))
Wie wir sehen können, haben wir in der interaktiven Shell einen Iterator stadt
generiert. Jeder Aufruf der Methode next(stadt)
gibt eine andere Stadt zurück. Nachdem die letzte Stadt, d. h. Stuttgart, erstellt wurde, löst ein weiterer Aufruf von next(stadt)
eine Ausnahme aus, die besagt, dass die Iteration gestoppt wurde, d. h. StopIteration
.
"Können wir einen Reset an einen Iterator senden?" ist eine häufig gestellte Frage, damit die Iteration von vorne beginnen kann. Es gibt kein Reset, aber es ist möglich, einneues Generatorobjekt zu erstellen, indem man erneut die Anweisung stadt = city_generator()
ausführen lässt.
Obwohl die yield
-Anweisung auf den ersten Blick wie die Return-Anweisung einer Funktion aussieht, können wir in diesem Beispiel sehen, dass es einen großen Unterschied gibt. Wenn wir im vorherigen Beispiel eine return-Anweisung anstelle eines yield
hätten, wäre dies eine Funktion. Aber diese Funktion würde immer nur die erste STadt, also "Hamburg" zurückliefern und niemals eine der anderen Städte, d. h. "Konstanz", "Berlin", "Zürich", "Schaffhausen" und "Stuttgart"
Funktionsweise¶
Wie wir in der Einleitung dieses Kapitels ausgeführt haben, bieten die Generatoren eine komfortable Möglichkeit zum Generieren von Iteratoren. Deshalb werden sie als Generatoren bezeichnet.
Arbeitsweise:
- Ein Generator wird aufgerufen wie eine Funktion. Sein Rückgabewert ist ein Iterator, d. h. ein Generatorobjekt. Der Code des Generators wird zu diesem Zeitpunkt nicht ausgeführt.
- Der Iterator kann durch Aufrufen der
next
-Methode aufgerufen werden. Beim ersten Aufruf wird mit der ersten Codezeile des Iteratorobjektes begonnen. Der Code wird solange ausgeführt, bis eineyield
-Anweisung erreicht wird. yield
gibt den Wert des Ausdrucks zurück, der dem Schlüsselwortyield
folgt. Beim nächsten Aufruf wird die Ausführung mit der Anweisung fortgesetzt, die auf dieyield
-Anweisung folgt, und die Variablen haben dieselben Werte wie beim vorherigen Aufruf.- Der Iterator ist beendet, wenn der Generatorkörper vollständig durchgearbeitet worden ist oder wenn der Programmablauf auf eine return-Anweisung ohne Wert stößt.
Wir werden dieses Verhalten im folgenden Beispiel veranschaulichen. Der Generator count
erstellt einen Iterator, der eine Folge von Werten erstellt, indem er mit dem Startwert firstval
zu zählen beginnt und den Wert von step
als Inkrement für die Zählung verwendet:
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()
Verwenden einer 'Return'-Anweisung in einem Generator¶
Seit Python 3.3 können Generatoren auch return-Anweisungen verwenden, aber ein Generator benötigt immer noch mindestens eine yield
-Anweisung, um ein Generator zu sein! Eine return
-Anweisung in einem Generator entspricht einer StopIteration
Schauen wir uns einen Generator an, in dem wir StopIteration
explizit auslösen:
def gen():
yield 1
raise StopIteration(42)
yield 2
g = gen()
next(g)
next(g)
Wir zeigen jetzt, dass die return
-Anweisung nahezu dem expliziten Auslösen der "StopIteration"-Ausnahme entspricht.
def gen():
yield 1
return 42
yield 2
g = gen()
next(g)
next(g)
send Methode / Coroutinen¶
Generatoren können nicht nur Objekte mittels yield an den Aufrufenden zurückliefern, sondern sie können auch Objekte empfangen. Das Senden einer Nachricht, d. h. eines Objekts, an den Generator kann mittels der send
-Funktion realisiert werden. Beachten Sie, dass send
sowohl einen Wert an den Generator sendet als auch den vom Generator mittels yield
gelieferten Wert zurückgibt. Wir werden dieses Verhalten im folgenden einfachen Beispiel eines Generators demonstrieren:
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.
Um die send
-Methode verwenden zu können, muss bereits ein yield
erfolgt sein, und dieses yield
muss auf der rechten Seite einer Zuweisung stehen. Erfolg dann ein next
oder send
wird ein Wert an die
Variable auf der linken Seite gesenden. Ein next
-Anruf sendet und empfängt ebenfalls. Von next
wird ein None-Objekt gesendet. Die von "next" und "send" gesendeten Werte werden einer Variablen im Generator zugewiesen: Diese Variable wird im folgenden Beispiel "new_counter_val" genannt.
Im folgenden Beispiel wird die Anzahl der Generatoren aus dem vorherigen Unterkapitel durch Hinzufügen einer Funktion "Senden" geändert.
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¶
Die throw
-Methode löst an der Stelle, an der der Generator angehalten wurde, eine Ausnahme aus und gibt den nächsten vom Generator mittels yield
ausgegebenen Wert zurück. Es löst StopIteration aus, wenn der Generator beendet wird, ohne auf ein yield
zu stoßen. Der Generator muss die übergebene Ausnahme abfangen, andernfalls wird die Ausnahme an den Aufrufer weitergegeben.
Der Iterator aus unserem vorherigen Beispiel liefert unaufhörlich die Elemente der sequentiellen Daten, aber wir haben keine Informationen über den Index oder den Status der Variablen von count
. Wir können diese Informationen erhalten, indem wir eine Ausnahme mit der throw
-Methode auslösen. Wir fangen diese Ausnahme im Generator ab und drucken die Werte der Variablen von count
:
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" ist seit Python 3.3 verfügbar!
Die yield from <expr>
-Anweisung kann im Körper eines Generators verwendet werden. <expr>
muss ein Ausdruck sein, der zu einem iterierbaren Objekt auswerten lässt, aus dem dann ein Iterator extrahiert wird.
Der Iterator wird ausgeführt bis er "exhausted" ist, d. h. bis er auf eine StopIteration
-Ausnahme stößt. Dieser Iterator liefert und empfängt Werte an oder von dem Aufrufer des Generators, d. h. derjenigen, der die yield from
-Anweisung enthält.
Wir können aus dem folgenden Beispiel lernen, indem wir uns die beiden Generatoren 'gen1' und 'gen2' ansehen, dass die yield from
-Anweisungen in gen2
die for-Schleifen von gen1
ersetzen:
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())
Dies würde dir sehr schnell die Grenzen deines Computers aufzeigen.
In den meisten praktischen Anwendungen benötigen wir nur die ersten n
Elemente von diesem und anderen "unendlichen" Iteratoren. r 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
min_ü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 != 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
min_ü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
fh = open("times_and_temperatures.txt", "w")
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 != 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))
Nächstes Kapitel: Anonyme Funktionen: Lambda-Operator, map- filter- und reduce