Fortgeschrittene Aspekte von Python¶
In diesem Kapitel sollen einige Sprachelemente von Python besprochen werden, auf die in der Vorlesung »Einführung in das Programmieren für Physiker und Materialwissenschaftler« nicht oder nur nur sehr kurz eingegangen wurde. Aus Platz- und Zeitgründen muss allerdings auch hier eine Auswahl getroffen werden.
Sets¶
In der Vorlesung »Einführung in das Programmieren für Physiker und Materialwissenschaftler« hatten wir uns im Kapitel über zusammengesetzte Datentypen vor allem mit Listen, Tupeln, Zeichenketten und Dictionaries beschäftigt. Sets wurden dagegen nur kurz erwähnt und sollen hier etwas ausführlicher besprochen werden.
Ein Set ist eine Menge von Python-Objekten, denen ein Hashwert zugeordnet werden kann. Insofern ist es mit einem Dictionary vergleichbar, das nur Schlüssel, aber nicht die zugehörigen Werte enthält. Die Einträge eines Sets können nicht mehrfach auftreten, so dass die Bildung eines Sets geeignet ist, um aus einer Liste Duplikate zu entfernen. Dies wird im Folgenden demonstriert.
In [1]: list_pts = [(0, 0), (-1, 2), (0, 0), (1, 2), (-1, 2), (0,0)]
In [2]: set_pts = set(list_pts)
In [3]: set_pts
Out[3]: {(-1, 2), (0, 0), (1, 2)}
In [4]: uniq_list_pts = list(set_pts)
In [5]: uniq_list_pts
Out[5]: [(1, 2), (0, 0), (-1, 2)]
Zunächst wird eine Liste erstellt, die hier Tupel enthält, um beispielsweise Punkte in der Ebene zu beschreiben. In der Eingabe 2 wird ein Set erstellt, in dem, wie man in der Ausgabe 3 sieht, tatsächlich keine Duplikate mehr vorkommen. Dabei liegen die Elemente im Set nicht in einer bestimmten Ordnung vor, ganz so wie wir es von Dictionaries kennen. Bei Bedarf kann man das Set auch wieder in eine Liste umwandeln, wie die Eingabe 4 und die Ausgabe 5 zeigen.
Statt wie im vorigen Beispiel ein Set durch Umwandlung aus einer Liste zu erzeugen, kann man die Elemente des Sets auch direkt in einer Notation mit geschweiften Klammern, die an die Verwandtschaft mit Dictionaries erinnert, eingeben.
In [1]: set_of_ints = {1, 3, 2, 5, 2, 1, 3, 4}
In [2]: set_of_ints
Out[2]: {1, 2, 3, 4, 5}
Auch hier werden natürlich eventuelle Dubletten entfernt.
Ähnlich wie Listen oder Dictionaries sind Sets auch veränderbar (mutable) und
damit selbst nicht als Elemente von Sets oder als Schlüssel von Dictionaries
verwendbar. Dafür kann man Elemente hinzufügen oder entfernen, wobei der
Versuch, ein nicht vorhandenes Element zu entfernen, eine KeyError
-Ausnahme
auslöst.
In [1]: data = {1, 2, 4}
In [2]: data.add(3)
In [3]: data
Out[3]: {1, 2, 3, 4}
In [4]: data.remove(1)
In [5]: data
Out[5]: {2, 3, 4}
In [6]: data.remove(10)
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-13-6610e4562113> in <module>()
----> 1 data.remove(10)
KeyError: 10
Will man ein Set als Schlüssel verwenden und ist man dafür bereit, auf die gerade
beschriebenen Möglichkeiten, ein Set zu verändern, zu verzichten, so greift man
auf das frozenset
zurück, das wie der Name schon andeutet unveränderlich
(immutable) ist.
In [1]: evens = frozenset([2, 4, 6, 8])
In [2]: evens.add(10)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-15-2c352b4e8a10> in <module>()
----> 1 evens.add(10)
AttributeError: 'frozenset' object has no attribute 'add'
In [3]: odds = frozenset([1, 3, 5, 7])
In [4]: numbers = {evens: "some even numbers", odds: "some odd numbers"}
In [5]: numbers.keys()
Out[5]: dict_keys([frozenset({8, 2, 4, 6}), frozenset({1, 3, 5, 7})])
Um zu überprüfen, ob ein Objekt Element einer Menge ist, ist es günstig, statt einer Liste ein Set zu verwenden, wie die folgenden Tests zeigen. [1]
In [1]: nmax = 1000000
In [2]: xlist = list(range(nmax))
In [3]: xset = set(xlist)
In [4]: %timeit 1 in xlist
10000000 loops, best of 3: 37 ns per loop
In [5]: %timeit 1 in xset
10000000 loops, best of 3: 32.1 ns per loop
In [6]: %timeit nmax-1 in xlist
100 loops, best of 3: 10.6 ms per loop
In [7]: %timeit nmax-1 in xset
10000000 loops, best of 3: 85.4 ns per loop
Hier liegen eine Liste und ein Set mit einer Million Elementen vor. Prüft man auf Mitgliedschaft eines der ersten Listenelemente ab, so gibt es keinen wesentlichen Unterschied zwischen Liste und Set. Ganz anders sieht es aus, wenn man ein Element vom Ende der Liste auswählt. In diesem Fall muss die ganze Liste durchsucht werden und die Ausführungszeit ist in unserem Beispiel mehr als hunderttausendmal langsamer als für das Set. Dieser Unterschied ist vor allem auch dann relevant, wenn das gewählte Element nicht vorhanden ist, so dass auf jeden Fall die gesamte Liste durchsucht werden muss. Natürlich ist die Erzeugung eines Sets mit einigem Zeitaufwand verbunden. Muss man aber häufig auf Mitgliedschaft in einer bestimmten Liste testen, so kann die Umwandlung in ein Set die Ausführung entscheidend beschleunigen.
Neben dem Test auf Mitgliedschaft lässt ein Set auch noch eine Reihe von
Operationen auf Mengen zu, wie zum Beispiel das Vereinigen zweier Mengen (union
oder |
), das Bilden der Schnittmenge (intersection
oder &
) und deren
Komplement (symmetric_difference
oder ^
) sowie das Bilden der Differenzmenge
(difference
oder -
). Zudem lässt sich auf Unter- und Obermenge (issubset
bzw. issuperset
) sowie Schnittmengenfreiheit (isdisjoint
) testen. Diese
Möglichkeiten sind im Folgenden illustriert.
In [1]: a = set([1, 2, 3])
In [2]: b = set([4, 5, 6])
In [3]: a.union(b)
Out[3]: {1, 2, 3, 4, 5, 6}
In [4]: c = set([1, 3, 6])
In [5]: a.intersection(c)
Out[5]: {1, 3}
In [6]: a.symmetric_difference(c)
Out[6]: {2, 6}
In [7]: a.difference(c)
Out[7]: {2}
In [8]: d = set([1, 3])
In [9]: a.issuperset(d)
Out[9]: True
In [10]: a.issubset(d)
Out[10]: False
In [11]: a.isdisjoint(b)
Out[11]: True
Das collections
-Modul¶
Die Standardbibliothek von Python stellt im collections
-Modul einige
interessante Container-Datentypen zur Verfügung, die es erlauben, Probleme
zu lösen, die gelegentlich mit Listen, Tupeln oder Dictionaries auftreten.
Im Folgenden soll eine Auswahl dieser Datentypen kurz vorgestellt werden.
Wir beginnen mit den Tupeln, deren einzelne Elemente mit Hilfe von Integern angesprochen werden können. Wenn die einzelnen Elemente eine spezielle Bedeutung haben, ist jedoch die Zuordnung zu den Indizes nicht immer offensichtlich.
Als Beispiel betrachten wir Farben, die im RGB-System durch ein Tupel von drei Integern mit Werten zwischen 0 und 255 dargestellt werden können. Eine bestimmte Farbe könnte also folgendermaßen durch ein Tupel repräsentiert sein:
In [1]: farbe = (135, 206, 235)
In [2]: farbe[1]
Out[2]: 206
In [3]: r, g, b = farbe
In [4]: g
Out[4]: 206
Hierbei muss man wissen, dass das Element mit Index 1 dem Grünwert entspricht. Um den Code verständlicher zu machen, kann man das Tupel auch wie in Eingabezeile 3 in die einzelnen Bestandteile zerlegen und diese entsprechend benennen. Es wäre jedoch praktischer, wenn man diesen Schritt nicht explizit vornehmen müsste.
In einem solchen Fall ist ein namedtuple
nützlich, um lesbaren Code zu
schreiben. In der Definition des namedtuple
zur Darstellung einer Farbe
ordnen wir den einzelnen Elementen Namen zu und können mit Hilfe dieser Namen
auf die Elemente zugreifen.
In [5]: import collections
In [6]: Farbe = collections.namedtuple('Farbe', 'r g b')
In [7]: f1 = Farbe(135, 206, 235)
In [8]: f1[1]
Out[8]: 206
In [9]: f1.g
Out[9]: 206
In [10]: f2 = Farbe(50, 205, 50)
In [11]: f1.b > f2.b
Out[11]: True
Gemäß der Definition in Eingabezeile 6 erhalten die Elemente die Bezeichner
r
, g
und b
und können dazu verwendet werden, auf die entsprechenden
Elemente zuzugreifen, wie in den Eingabezeilen 9 und 11 zu sehen ist. Es ist
jedoch auch weiterhin möglich, wie in Eingabezeile 8 auf ein Element mit Hilfe
seines Index zuzugreifen.
Die Bezeichner sind nicht nur auf einzelne Buchstaben beschränkt, sondern können bei Bedarf noch aussagekräftiger gewählt werden.
In [12]: Farbe = collections.namedtuple('Farbe', 'rot grün blau')
In [13]: f1 = Farbe(135, 206, 235)
In [14]: f1.grün
Out[14]: 206
Im Unterschied zum Dictionary ist das namedtuple
, genauso wie das Tuple,
immutable, kann also zum Beispiel als Schlüssel für ein Dictionary verwendet
werden. Zudem ist es so speichereffizient wie ein normales Tuple.
In [15]: f1.rot = 20
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-15-ac8b7e672be5> in <module>()
----> 1 f1.rot = 20
AttributeError: can't set attribute
In [16]: f2 = Farbe(50, 205, 50)
In [17]: rgbdict = {f1: 'SkyBlue', f2: 'LimeGreen'}
In [18]: rgbdict[Farbe(135, 206, 235)]
Out[18]: 'SkyBlue'
Ein anderes Problem tritt in Zusammenhang mit Listen auf. Während der Zeitaufwand für das Anhängen eines neuen Elements sehr klein ist, kann das Einfügen eines Elements am Anfang der Liste sehr zeitaufwändig sein wie das folgende Beispiel zeigt.
In [19]: %%timeit
...: xlist = list()
...: for n in range(100000):
...: xlist.append(n)
...:
100 loops, best of 3: 7.66 ms per loop
In [20]: %%timeit
...: xlist = list()
...: for n in range(100000):
...: xlist.insert(0, n)
...:
1 loop, best of 3: 2.63 s per loop
In einem solchen Fall kann ein so genanntes deque
, dessen Name sich von
double-ended queue [2] ableitet, erhebliche Vorteile bringen, so lange
man Elemente an einem der beiden Enden hinzufügt oder entfernt.
In [21]: %%timeit
...: xdeq = collections.deque()
...: for n in range(100000):
...: xdeq.append(n)
...:
100 loops, best of 3: 7.57 ms per loop
In [22]: %%timeit
...: xdeq = collections.deque()
...: for n in range(100000):
...: xdeq.appendleft(n)
...:
100 loops, best of 3: 7.61 ms per loop
Eine mögliche Anwendung ist ein FIFO (first in, first out), das Objekte aufnehmen kann und in dieser Reihenfolge auch wieder zurückgibt.
In [23]: xdeq = collections.deque([2, 1])
In [24]: xdeq.appendleft(3)
In [25]: xdeq
Out[25]: deque([3, 2, 1])
In [26]: xdeq.pop()
Out[26]: 1
In [27]: xdeq
Out[27]: deque([3, 2])
In [28]: xdeq.pop()
Out[28]: 2
In [29]: xdeq
Out[29]: deque([3])
In [30]: xdeq.appendleft(4)
In [31]: xdeq
Out[31]: deque([4, 3])
In [32]: xdeq.pop()
Out[32]: 3
In [33]: xdeq
Out[33]: deque([4])
Auch das Rotieren eines deque
ist leicht möglich.
In [34]: xdeq = collections.deque(range(10))
In [35]: xdeq
Out[35]: deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [36]: xdeq.rotate(3)
In [37]: xdeq
Out[37]: deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6])
In [38]: xdeq.rotate(-5)
In [39]: xdeq
Out[39]: deque([2, 3, 4, 5, 6, 7, 8, 9, 0, 1])
Abschließend wollen wir noch kurz das OrderedDict
erwähnen. Bei Dictionaries
sind die Schlüssel im Allgemeinen nicht geordnet. Ein OrderedDict
dagegen merkt sich, in welcher Reihenfolge die Einträge hinzugefügt wurden.
In [40]: nobelpreise = dict([('Marie Curie', 1903),
...: ('Maria Goeppert Mayer', 1963),
...: ('Klaus von Klitzing', 1985),
...: ('Albert Einstein', 1921)])
In [41]: for preis in nobelpreise:
...: print(preis)
...:
Maria Goeppert Mayer
Marie Curie
Albert Einstein
Klaus von Klitzing
In [42]: nobelpreise = collections.OrderedDict([('Marie Curie', 1903),
...: ('Maria Goeppert Mayer', 1963),
...: ('Klaus von Klitzing', 1985),
...: ('Albert Einstein', 1921)])
In [43]: for preis in nobelpreise:
...: print(preis)
...:
Marie Curie
Maria Goeppert Mayer
Klaus von Klitzing
Albert Einstein
Es gibt die Möglichkeit, mit der Methode move_to_end()
einen einzelnen
Eintrag an das Ende eines OrderedDict
zu verschieben. Um ein bestehendes
Dictionary oder OrderedDict
umzusortieren, erzeugt man am besten ein neues
OrderedDict
.
In [44]: nobelpreise_sorted = collections.OrderedDict(
...: sorted(nobelpreise.items(),
...: key=lambda x: x[1]))
In [45]: for name, jahr in nobelpreise_sorted.items():
...: print(jahr, name)
...:
1903 Marie Curie
1921 Albert Einstein
1963 Maria Goeppert Mayer
1985 Klaus von Klitzing
List comprehensions¶
Um eine Liste aufzubauen, kann man sich zum Beispiel der folgenden Konstruktion bedienen.
In [1]: squares = []
In [2]: for n in range(10):
...: squares.append(n*n)
...:
In [3]: squares
Out[3]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Hierbei wird zunächst eine leere Liste angelegt, an die anschließend in einer Schleife die Quadratzahlen angefügt werden. Etwas kompakter und damit auch übersichtlicher kann man diese Funktionalität mit Hilfe einer so genannten list comprehension [3] erreichen.
In [1]: squares = [n*n for n in range(10)]
In [2]: squares
Out[2]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Liest man den Text in den eckigen Klammern in Eingabe 1, so bekommt man eine
sehr klare Vorstellung davon, was dieser Code bewirken soll. Vor der
for
-Anweisung kann auch eine andere Anweisung stehen, die die Listenelemente
erzeugt.
In [1]: from math import pi, sin
In [2]: [(0.1*pi*n, sin(0.1*pi*n)) for n in range(6)]
Out[2]: [(0.0, 0.0),
(0.3141592653589793, 0.3090169943749474),
(0.6283185307179586, 0.5877852522924731),
(0.9424777960769379, 0.8090169943749475),
(1.2566370614359172, 0.9510565162951535),
(1.5707963267948966, 1.0)]
List comprehensions sind nicht nur häufig übersichtlicher, sondern in der Ausführung auch etwas schneller.
In [1]: %%timeit result = []
...: for n in range(1000):
...: result.append(n*n)
...:
10000 loops, best of 3: 91.8 µs per loop
In [2]: %timeit result = [n*n for n in range(1000)]
10000 loops, best of 3: 54.4 µs per loop
In unserem Fall ist die list comprehension also um fast einen Faktor 1,7 schneller.
Die Syntax von list comprehensions ist nicht auf die bisher beschriebenen einfachen Fälle beschränkt. Sie lässt zum Beispiel auch das Schachteln von Schleifen zu.
In [1]: [x**y for y in range(1, 4) for x in range(2, 5)]
Out[1]: [2, 3, 4, 4, 9, 16, 8, 27, 64]
In [2]: result = []
In [3]: for y in range(1, 4):
...: for x in range(2, 5):
...: result.append(x**y)
...:
In [4]: result
Out[4]: [2, 3, 4, 4, 9, 16, 8, 27, 64]
Wie man sieht, sind die for
-Schleifen in der list comprehension von der
äußersten zur innersten Schleife anzugeben, wobei man auch mehr als zwei Schleifen
schachteln kann.
Man kann das Hinzufügen zur Liste zusätzlich noch von Bedingungen abhängig machen. Im folgenden Beispiel wird das Tupel nur in die Liste aufgenommen, wenn die erste Zahl ohne Rest durch die zweite Zahl teilbar ist.
In [1]: [(x, y) for x in range(1, 11) for y in range(2, x) if x % y == 0]
Out[1]: [(4, 2), (6, 2), (6, 3), (8, 2), (8, 4), (9, 3), (10, 2), (10, 5)]
Als kleines Anwendungsbeispiel betrachten wir den Quicksort-Algorithmus zur Sortierung von Listen. Die Idee hierbei besteht darin, ein Listenelement zu nehmen und die kleineren Elemente in einer rekursiv sortierten Liste diesem Element voranzustellen und die anderen Elemente sortiert anzuhängen.
In [1]: def quicksort(x):
...: if len(x) < 2: return x
...: return (quicksort([y for y in x[1:] if y < x[0]])
...: +x[0:1]
...: +quicksort([y for y in x[1:] if x[0] <= y]))
In [2]: import random
In [3]: liste = [random.randint(1, 100) for n in range(10)]
In [4]: liste
Out[4]: [51, 93, 66, 62, 46, 87, 91, 41, 3, 40]
In [5]: quicksort(liste)
Out[5]: [3, 40, 41, 46, 51, 62, 66, 87, 91, 93]
Das Konzept der list comprehension lässt sich auch auf Sets und Dictionaries übertragen. Letzteres ist im folgenden Beispiel gezeigt.
In [25]: s = 'Augsburg'
In [26]: {x: s.count(x) for x in s}
Out[26]: {'A': 1, 'b': 1, 'r': 1, 's': 1, 'u': 2, 'g': 2}
Wie wir gesehen haben, kann eine list comprehension zum einen aus einer Liste
durch Anwendung einer Funktion eine andere Liste machen und zum anderen
Listenelemente zur Aufnahme in die neue Liste mit Hilfe einer Bedingung
auswählen. Diese beiden Komponenten können gemeinsam oder auch einzeln
vorkommen. In letzterem Fall kann man alternativ die map
-Funktion bzw. die
filter
-Funktion verwenden. Beide sind zentrale Elemente des so genannten
funktionalen Programmierens.
map
wendet die im ersten Argument angegebene Funktion auf die im zweiten
Argument angegebene Liste an. Um eine Liste von Quadratzahlen zu erzeugen,
kann man statt einer expliziten for
-Schleife auch eine der beiden
folgenden Möglichkeiten verwenden:
In [1]: [x*x for x in range(1, 11)]
Out[1]: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
In [2]: quadrate = map(lambda x: x*x, range(1, 11))
In [3]: list(quadrate)
Out[3]: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Eine nützliche Anwendung der map
-Funktion besteht darin, die nach dem
Einlesen numerischer Daten zunächst vorhandenen Strings in Floats umzuwandeln:
In [1]: s = "0.1 0.2 0.4 -0.5"
In [2]: zeilenelemente = map(float, s.split())
In [3]: list(zeilenelemente)
Out[3]: [0.1, 0.2, 0.4, -0.5]
Bei der filter
-Funktion muss die als erstes Argument angegebene Funktion
einen Wahrheitswert zurückgeben, der darüber entscheidet, ob ein Element
der Sequenz im zweiten Argument übernommen wird oder nicht.
In [1]: initialen = filter(lambda x: x.isupper(), 'Albert Einstein')
In [2]: "".join(initialen)
Out[2]: 'AE'
Zur Abwechslung haben wir hier statt einer Liste eine Zeichenkette verwendet, die Zeichen für Zeichen abgearbeitet wird. Das Ergebnis enthält die Großbuchstaben der Zeichenkette.
Zu den wesentlichen Elementen des funktionalen Programmierens gehört auch die
reduce
-Funktion. Während sie in Python 2 noch zum normalen Sprachumfang
gehörte, muss sie in Python 3 aus dem functools
-Modul importiert werden
[4]. Tatsächlich gibt es für viele Anwendungsfälle angepasste
Funktionen als Ersatz, wie wir gleich noch sehen werden.
Als erstes Argument muss reduce
eine Funktion bekommen, die
zwei Argumente verarbeitet. reduce
wendet dann die Funktion auf die ersten
beiden Elemente der als zweites Argument angegebenen Liste an, verarbeitet dann
entsprechend das Ergebnis und das dritte Element der Liste und fährt so fort
bis das Ende der Liste erreicht ist. Die folgende Implementation der Fakultät
illustriert dies.
In [1]: import functools
In [2]: factorial = lambda n: functools.reduce(lambda x, y: x*y, range(1, n+1))
In [3]: factorial(6)
Out[3]: 720
Entsprechend lässt sich auch die Summe der Elemente einer Liste bestimmen.
In [1]: reduce(lambda x, y: x+y, [0.1, 0.3, 0.7])
Out[1]: 1.1
In [2]: sum([0.1, 0.3, 0.7])
Out[2]: 1.1
Wie die zweite Eingabe zeigt, stellt Python zu diesem Zweck auch direkt die
sum
-Funktion zur Verfügung. Ähnliches gilt für die Verwendung der Oder- und
der Und-Verknüpfung in der reduce
-Funktion, die direkt durch die any
-
bzw. all
-Funktion abgedeckt werden.
In [1]: any([x % 2 for x in [2, 5, 6]])
Out[1]: True
In [2]: all([x % 2 for x in [2, 5, 6]])
Out[2]: False
In der ersten Eingabe wird überprüft, ob mindestens ein Element der Liste ungerade ist, während die zweite Eingabe überprüft, ob alle Elemente ungerade sind.
Zum Abschluss dieses Kapitels wollen wir noch zwei äußerst praktische
eingebaute Funktionen erwähnen, die, falls sie nicht existieren würden, mit
list comprehensions realisiert werden könnten. Häufig benötigt man bei der
Iteration über eine Liste in einer for
-Schleife noch den Index des
betreffenden Eintrags. Dies lässt sich mit Hilfe der enumerate
-Funktion
sehr einfach realisieren.
In [1]: for nr, text in enumerate(['eins', 'zwei', 'drei']):
...: print(nr+1, text)
...:
1 eins
2 zwei
3 drei
Die enumerate
-Funktion gibt also für jedes Element der Liste ein Tupel
zurück, das aus dem Index und dem entsprechenden Element besteht. Dabei beginnt
die Zählung wie immer in Python bei Null.
Es kommt auch immer wieder vor, dass man zwei oder mehr Listen parallel in einer
for
-Schleife abarbeiten möchte. Dann ist die zip
-Funktion von Nutzen,
die aus den Einträgen mit gleichem Index nach dem Reißverschlussprinzip Tupel
zusammenbaut.
In [1]: a = [1, 2, 3]
In [2]: b = [4, 5, 6]
In [3]: ab = zip(a, b)
In [4]: list(ab)
Out[4]: [(1, 4), (2, 5), (3, 6)]
Sollten die beteiligten Listen verschieden lang sein, so ist die Länge der neuen Liste durch die kürzeste der eingegebenen Listen bestimmt.
Man kann die zip
-Funktion zum Beispiel dazu verwenden, um elegant
Mittelwerte aus aufeinanderfolgenden Listenelementen zu berechnen.
In [1]: data = [1, 4, 5, 3, -1, 2]
In [2]: for x, y in zip(data[:-1], data[1:]):
...: print((x+y)/2)
...:
2.5
4.5
4.0
1.0
0.5
Generatoren und Iteratoren¶
Es kommt häufig vor, dass man Listen mit einer list comprehension erzeugt, nur um anschließend über diese Liste zu iterieren. Dabei reicht es völlig aus, wenn die jeweiligen Elemente erst bei Bedarf erzeugt werden. Somit ist es nicht mehr erforderlich, die ganze Liste im Speicher bereitzuhalten, was bei großen Listen durchaus zum Problem werden könnte.
Will man die Listenerzeugung vermeiden, so kann man statt einer list comprehension einen Generatorausdruck verwenden. Die beiden unterscheiden sich syntaktisch dadurch, dass die umschließenden eckigen Klammern der list comprehension durch runde Klammern ersetzt werden.
In [1]: quadrate = (x*x for x in xrange(4))
In [2]: quadrate
Out[2]: <generator object <genexpr> at 0x39e4a00>
In [3]: for q in quadrate:
...: print q
...:
0
1
4
9
Man kann die Werte des Generatorausdruck auch explizit durch Verwendung der
zugehörigen __next__
-Methode abrufen. Allerdings sind die Werte nach dem obigen
Beispiel bereits abgearbeitet, so dass die __next__
-Methode in einer
StopIteration
-Ausnahme resultiert. Damit wird angezeigt, dass bereits alle
Wert ausgegeben wurden. Die StopIteration
-Ausnahme war auch in der
for
-Schleife verantwortlich dafür, dass diese beendet wurde.
In [4]: next(quadrate)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-4-ec579e92187a> in <module>()
----> 1 quadrate.next()
StopIteration:
In [5]: quadrate = (x*x for x in xrange(4))
In [6]: next(quadrate)
Out[6]: 0
In [7]: next(quadrate)
Out[7]: 1
In [8]: next(quadrate)
Out[8]: 4
In [9]: next(quadrate)
Out[9]: 9
In [10]: next(quadrate)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-10-ec579e92187a> in <module>()
----> 1 quadrate.next()
StopIteration:
In Eingabe 5 wurde der Generatorausdruck neu initialisiert, so dass er wieder
vier Werte liefern konnte. Am Ende wird dann wiederum die
StopIteration
-Ausnahme ausgelöst. Natürlich kann man diese Ausnahme auch
abfangen, wie in folgendem Beispiel gezeigt wird.
In [1]: def q():
...: try:
...: return quadrate.next()
...: except StopIteration:
...: return "Das war's mit den Quadratzahlen."
...:
In [2]: quadrate = (x*x for x in xrange(4))
In [3]: [q() for n in range(5)]
Out[3]: [0, 1, 4, 9, "Das war's mit den Quadratzahlen."]
Aus Sequenzen kann man mit Hilfe der eingebauten iter
-Funktion Iteratoren
konstruieren.
In [1]: i = iter([1, 2, 3])
In [2]: next(i)
Out[2]: 1
In [3]: next(i)
Out[3]: 2
In [4]: next(i)
Out[4]: 3
In [5]: next(i)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-5-e590fe0d22f8> in <module>()
----> 1 next(i)
StopIteration:
In der Eingabe 1 wird ein Iterator erzeugt, der über eine __next__
-Methode
verfügt und nach dem Abarbeiten der Liste eine StopIteration
-Ausnahme
auslöst. Iteratoren kann man auch über eine Klassendefinition erhalten, wie im
folgenden Beispiel für die Fibonacci-Zahlen gezeigt ist.
In [1]: class Fibonacci(object):
...: def __init__(self, nmax):
...: self.nmax = nmax
...: self.a = 0
...: self.b = 1
...: def __iter__(self):
...: return self
...: def __next__(self):
...: if self.nmax == 0:
...: raise StopIteration
...: self.b, self.a = self.b+self.a, self.b
...: self.nmax = self.nmax-1
...: return self.a
...:
In [2]: for n in Fibonacci(10):
...: print(n, end=' ')
...:
1 1 2 3 5 8 13 21 34 55
Die __iter__
-Methode dieser Klasse gibt sich selbst zurück, während die
__next__
-Methode das jeweils nächste Element zurückgibt. Nachdem die
ersten nmax
Elemente der Fibonacci-Reihe erzeugt wurden, wird eine
StopIteration
-Ausnahme ausgelöst, die zur Beendung der for
-Schleife in
Eingabe 2 führt.
Normalerweise wird es einfacher sein, statt einer solchen Klassendefinition
einen Generator zu schreiben. Dieser sieht auf den ersten Blick wie eine
Funktionsdefinition aus. Allerdings ist die return
-Anweisung durch eine
yield
-Anweisung ersetzt, die dafür verantwortlich ist, den jeweils nächsten
Wert zurückzugeben. Bemerkenswert ist im Vergleich zu Funktionen außerdem, dass
die Werte der Funktionsvariablen nicht verlorengehen. Das folgende Beispiel
erzeugt die ersten Zeilen eines pascalschen Dreiecks.
In [1]: def pascaltriangle(n):
...: coeff = 1
...: yield coeff
...: for m in range(n):
...: coeff = coeff*(n-m)//(m+1)
...: yield coeff
In [2]: for n in range(11):
...: print " ".join(str(p).center(3) for p in pascaltriangle(n)).center(50)
...:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
1 9 36 84 126 126 84 36 9 1
1 10 45 120 210 252 210 120 45 10 1
In der letzten Zeile fungieren die Klammern der join
-Methode gleichzeitig als Klammern
für den Generatorausdruck.
Man kann yield
auch benutzen, um Werte in die Funktion
einzuspeisen. Auf diese Weise erhält man eine Koroutine. Dieses Konzept soll
hier jedoch nicht weiter diskutiert werden.
Abschließend sei noch erwähnt, dass das itertools
-Modul eine ganze Reihe von
nützlichen Iteratoren zur Verfügung stellt. Als Beispiel mögen Permutationen dienen.
In [1]: import itertools
In [2]: for s in itertools.permutations("ABC"):
...: print s
...:
('A', 'B', 'C')
('A', 'C', 'B')
('B', 'A', 'C')
('B', 'C', 'A')
('C', 'A', 'B')
('C', 'B', 'A')
Dekoratoren¶
Dekoratoren sind ein Programmierkonstrukt, das man gelegentlich gewinnbringend einsetzen kann, wie wir im Folgenden sehen werden. Aber selbst wenn man keine eigenen Dekoratoren programmieren möchte, sollte man zumindest das Konzept kennen. Es kommt immer wieder vor, dass bei der Verwendung von fremden Python-Paketen Dekoratoren zum Einsatz kommen. Dies kann dann zum Beispiel folgendermaßen aussehen:
@login_required
def myfunc():
"""this function should only be executable by users properly logged in"""
pass
Der Operator @
weist hier auf die Verwendung eines Dekorators hin.
Bevor wir uns aber mit Dekoratoren beschäftigen, ist es nützlich, so genannte Closures [5] zu diskutieren. Das Konzept soll an einem einfachen Beispiel erläutert werden.
In [1]: def add_tax(taxrate):
...: def _add_tax(value):
...: return value*(1+0.01*taxrate)
...: return _add_tax
...:
In [2]: add_mwst = add_tax(19)
In [3]: add_reduzierte_mwst = add_tax(7)
In [4]: for f in [add_mwst, add_reduzierte_mwst]:
...: print('{:.2f}'.format(f(10)))
...:
11.90
10.70
Mit add_tax
haben wir hier eine Funktion definiert, die wiederum eine
Funktion zurückgibt. Das Interessante an dieser Konstruktion ist, dass die
zurückgegebene Funktion sich den Kontext merkt, in dem sie erzeugt wurde. In
unserem Beispiel bedeutet das, dass die Funktion _add_tax
auf den Wert der
Variable taxrate
, also den Steuersatz, auch später noch zugreifen kann.
Dies wird deutlich, wenn wir zur Addition des vollen Mehrwertsteuersatzes die
Funktion add_mwst
definieren. Hierbei wird der Variable taxrate
der
Wert 19 mitgegeben, der später beim Aufruf von add_mwst
noch zur Verfügung
steht. Entsprechend definieren wir eine Funktion zur Addition des reduzierten
Mehrwertsteuersatzes. Am Beispiel der abschließenden Schleife wird deutlich,
dass die Funktionen wie gewünscht funktionieren.
Kommen wir nun zurück zu den Dekoratoren. Diese erlauben es, Funktionen oder Klassen mit Zusatzfunktionalität zu versehen oder diese zu modifizieren. Wir wollen uns hier auf Funktionen beschränken. Betrachten wir als ein erstes Beispiel den folgenden Code:
In [1]: def register(func):
...: print('{} registered'.format(func.__name__))
...: return func
...:
In [2]: @register
...: def myfunc():
...: print('executing myfunc')
...:
myfunc registered
In [3]: myfunc()
executing myfunc
In [4]: @register
...: def myotherfunc():
...: print('executing myotherfunc')
...:
myotherfunc registered
In [5]: myotherfunc()
executing myotherfunc
Hier haben wir zunächst einen Dekorator register
definiert, der als
Argument eine Funktion erhält. Bevor er sie unverändert zurückgibt, registriert
er die Funktion, was hier durch eine einfache Ausgabe nur angedeutet wird. Der
Dekorator kann nun verwendet werden, indem vor der gewünschten Funktion die
Zeile @register
eingefügt wird. Wie schon erwähnt, gibt der Operator @
an, dass hier ein Dekorator verwendet wird, die folgende Funktion also
dekoriert wird. In der Eingabe 2 wird myfunc
als Argument an register
übergeben. Bei der Auswertung der Funktionsdefinition wird nun der
Ausgabebefehl ausgeführt. Später erfolgt dies nicht mehr, da der Dekorator
register
die Funktion ja unverändert zurückgegeben hat. Die Eingabe 4
zeigt, dass der Dekorator mit einer beliebigen Funktion verwendet werden kann.
Wenden wir uns nun einem etwas komplexeren Beispiel zu. Wir haben in dem untenstehenden Code-Beispiel in den Zeilen 17-21 die Minimalvariante einer rekursiven Funktion für die Berechnung der Fakultät programmiert. Diese Funktion soll nun so modifiziert werden, dass Logging-Information ausgegeben wird. Zu Beginn der Funktion soll ausgegeben werden, mit welchem Argument die Funktion aufgerufen wurde und am Ende sollen zusätzlich das Ergebnis und die seit dem Aufruf verstrichene Zeit ausgegeben werden.
Natürlich könnte die entsprechende Funktionalität direkt in die Funktion programmiert werden, aber es spricht einiges dagegen, so vorzugehen. Die Fähigkeit, Logging-Information auszugeben, hat nichts mit der Berechnung der Fakultät zu tun, und daher ist es besser, die beiden Funktionalitäten sauber zu trennen. Dies wird deutlich, wenn man bedenkt, dass die Ausgabe von Logging-Information vor allem in der Entwicklungsphase erforderlich ist und später wahrscheinlich entfernt werden soll. Dann müsste man wieder in das Innere der Funktion eingreifen und die richtigen Zeilen identifizieren, die entfernt werden müssen. Außerdem ist die Ausgabe von Logging-Information etwas, was nicht nur für unsere spezielle Funktion nützlich ist, sondern auch in anderen Fällen verwendet werden kann. Dies spricht wiederum dafür, diese Funktionalität aus der eigentlichen Funktion fernzuhalten.
Genau dieses Ziel ist in dem folgenden Code realisiert, in dem die Funktion
factorial
mit einem Dekorator versehen ist.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import time
from itertools import chain
def logging(func):
def func_with_log(*args, **kwargs):
argumente = ', '.join(map(repr, chain(args, kwargs.items())))
print('calling {}({})'.format(func.__name__, argumente))
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time()-start
print('got {}({}) = {} in {:5.3f} ms'.format(
func.__name__, argumente, result, elapsed*1000
))
return result
return func_with_log
@logging
def factorial(n):
if n == 1:
return 1
else:
return n*factorial(n-1)
factorial(5)
|
Sehen wir uns nun also den Dekorator logging
in den Zeilen 4-14 an. Wie
schon angedeutet, wird die Funktion factorial
als Argument func
an die
Funktion logging
übergeben. Der wesentliche Teil von logging
besteht
darin, eine neue Funktion, die hier den Namen func_with_log
trägt, zu
definieren, die die Funktion func
ersetzen wird. Die Definition in den Zeilen
5-13 ist absichtlich allgemeiner gehalten als es für uns Beispiel notwendig
wäre. So lässt sich der Dekorator auch in anderen Fällen direkt einsetzen.
Daher lassen wir in Zeile 5 eine allgemeine Übergabe von Variablen in einem
Tupel args
und einem Dictionary kwargs
zu.
Die zugehörigen Werte werden in der Zeile 6 durch Kommas separiert zu einem
String zusammengebaut. Dabei übernimmt die aus dem itertools
-Modul
importierte Funktion chain
die Aufgabe, die Elemente des Tupels args
und der
Schlüssel-Wert-Tupel des Dictionaries kwargs
zu einer einzigen Sequenz
zusammenzufassen. Mit Hilfe der map
-Funktion wird zu jedem Element mit
Hilfe der repr
-Funktion die zugehörige Darstellung erzeugt. Die
join
-Funktion baut diese Darstellung schließlich zu einem String zusammen.
Nachdem dieses Verfahren etwas komplexer ist, sei angemerkt, dass dieses
Vorgehen nichts mit dem Dekorator an sich zu tun hat. Vielmehr ist es durch
unsere Anforderung bedingt, Logging-Information einschließlich der
Aufrufparameter ausgeben zu können, und dies nicht nur für die
factorial
-Funktion, sondern in einem möglichst allgemeinen Fall. Die
Ausgabe der Logging-Information erfolgt in Zeile 7. Zeile 8 bestimmt den
Startzeitpunkt. In diesem Zusammenhang wurde in Zeile 1 das time
-Modul
importiert.
Nach diesen Vorarbeiten wird in Zeile 9 die eigentliche Funktion, die die Fakultät berechnen soll, aufgerufen. Typischerweise wird die ursprüngliche Funktion tatsächlich aufgerufen. Allerdings ist dies nicht unbedingt notwendig. Man könnte stattdessen hier einfach einen Hinweis ausgeben, dass nun die Fakultät zu berechnen wäre. Auf diese Weise würde man jedoch kein Ergebnis für die Fakultät erhalten.
Nachdem die factorial
-Funktion ihr Ergebnis zurückgegeben hat, wird in
Zeile 10 die verstrichene Zeit bestimmt und in Zeile 11 in Millisekunden
gemeinsam mit dem Ergebnis ausgegeben. Abschließend soll die dekorierte
Funktion das berechnete Resultat zurückgeben. Damit ist die Definition der
dekorierten Funktion beendet und der Dekorator gibt diese Funktion in Zeile 14
zurück.
Ruft man in Zeile 24 nun die Funktion factorial
auf, so wird wegen des
logging
-Dekorators in Wirklichkeit die gerade besprochene, dekorierte
Funktion ausgeführt. Man erhält somit die folgende Ausgabe:
calling factorial(5)
calling factorial(4)
calling factorial(3)
calling factorial(2)
calling factorial(1)
got factorial(1) = 1 in 0.004 ms
got factorial(2) = 2 in 0.085 ms
got factorial(3) = 6 in 0.163 ms
got factorial(4) = 24 in 0.281 ms
got factorial(5) = 120 in 0.524 ms
In dieser Ausgabe ist gut zu sehen, wie durch die rekursive Abarbeitung
nacheinander die Fakultät von 5, von 4, von 3, von 2 und von 1 berechnet
wird. Die Ausführungen sind geschachtelt, denn die Berechnung der Fakultät
von 2 kann erst beendet werden, wenn die Fakultät von 1 bestimmt wurde.
Entsprechend benötigt die Berechnung der Fakultät von 5 auch mehr Zeit
als die Berechnung der Fakultät von 4 usw. Der logging
-Dekorator erlaubt
somit Einblicke in die Abarbeitung der rekursiven Funktion ohne dass
wir in diese Funktion direkt eingreifen mussten.
Abschließend betrachten wir noch ein weiteres Anwendungsbeispiel, das besonders dann von Interesse ist, wenn die Ausführung einer Funktion relativ aufwändig ist. Dann kann es sinnvoll sein, Ergebnisse aufzubewahren und auf diese bei einem erneuten Aufruf mit den gleichen Argumenten wieder zuzugreifen. Dies setzt natürlich eine deterministische Funktion voraus, also eine Funktion, deren Ergebnis nur von den übergebenen Argumenten abhängt. Außerdem wird der Gewinn an Rechenzeit mit Speicherplatz bezahlt. Dies ist jedoch normalerweise unproblematisch, so lange sich die Zahl verschiedener Argumentwerte in Grenzen hält.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def _memoize(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return _memoize
@logging
@memoize
def factorial(n):
if n == 1:
return 1
else:
return n*factorial(n-1)
|
Im memoize
-Dekorator ist hier eine Closure realisiert, die es der Funktion
_memoize
erlaubt, auch später auf das Dictionary cache
zuzugreifen, in
dem die Ergebnisse gespeichert werden. Hierzu eignet sich ein Dictionary, weil
man die Argumente in Form des Tupels args
als Schlüssel hinterlegen kann.
Allerdings ist es nicht möglich, auch ein eventuelles Dictionary kwargs
im Schlüssel unterzubringen. Argumente, die mit Schlüsselworten übergeben
werden, sind hier somit nicht erlaubt.
Aus den Zeilen 14 und 15 ersieht man, dass Dekoratoren auch geschachtelt werden
können. Die Funktion factorial
wird zunächst mit dem memoize
-Dekorator
versehen. Die so dekorierte Funktion wird dann mit dem logging
-Dekorator
versehen. Eine Schwierigkeit besteht hier allerdings darin, dass der
logging
-Dekorator für ein korrektes Funktionieren den Namen der ursprünglichen
Funktion, also factorial
benötigt. Aus diesem Grunde verwendet man in Zeile 5
den wraps
-Dekorator aus dem functools
-Modul, der dafür sorgt, dass Name
und Dokumentationsstring diejenigen der Funktion func
und nicht der Funktion
_memoize
sind.
Im Folgenden ist die Funktionsweise des memoize
-Dekorators gezeigt.
In [1]: factorial(4)
calling factorial(4)
calling factorial(3)
calling factorial(2)
calling factorial(1)
got factorial(1) = 1 in 0.005 ms
got factorial(2) = 2 in 0.063 ms
got factorial(3) = 6 in 0.252 ms
got factorial(4) = 24 in 0.369 ms
Out[2]: 24
In [2]: factorial(3)
calling factorial(3)
got factorial(3) = 6 in 0.007 ms
Out[3]: 6
Beim ersten Aufruf der Funktion factorial
wird die Fakultät rekursiv
ausgewertet wie wir das schon weiter oben gesehen haben. Dabei werden aber die
berechneten Werte im Dictionary cache
gespeichert. Ruft man nun die
Funktion mit einem Argument auf, dessen Fakultät bereits berechnet wurde,
kann direkt auf das Ergebnis im Cache zugegriffen werden. Dies zeigt sich
daran, dass keine rekursive Berechnung mehr durchgeführt wird und die
benötigte Zeit bis zur Rückgabe des Ergebnisses sehr kurz ist.
Abschließend sei noch kurz erwähnt, dass Dekoratoren auch ein Argument haben können, das wie üblich in Klammern angegeben wird. Dabei ist zu beachten, dass dem Dekorator dann nicht mehr wie in den hier diskutierten Beispielen die zu dekorierende Funktion übergeben wird, sondern das angegebene Argument. Die zu dekorierende Funktion wird dafür dann an die im Dekorator definierte Funktion übergeben.
Ausnahmen¶
Bereits in der »Einführung in das Programmieren« hatten wir Ausnahmen (exceptions)
kennengelernt und gesehen, wie man mit einem except
-Block auf eine Ausnahme
reagieren kann. Zudem haben wir im Abschnitt Generatoren und Iteratoren eine Anwendung
gesehen, in der wir selbst eine Ausnahme ausgelöst haben, nämlich eine
StopIteration
-Ausnahme. Im Folgenden sollen noch einige Aspekte von Ausnahmen
diskutiert werden, die bis jetzt zu kurz kamen.
Grundsätzlich ist es sinnvoll, möglichst spezifisch auf Ausnahmen zu reagieren.
Daher sollte zum einen der try
-Block kurz gehalten werden, um einen
möglichst direkten Zusammenhang zwischen Ausnahme und auslösendem Code zu
garantieren. Zum anderen sollten nicht unnötig viele Ausnahmen gleichzeitig in
einem except
-Block abgefangen werden.
In [1]: try:
...: datei = open('test.dat')
...: except IOError as e:
...: print('abgefangener Fehler:', e)
...: else:
...: content = datei.readlines()
...: datei.close()
...:
In [2]: content
Out[2]: ['Das ist der Inhalt der Test-Datei.\n']
In [3]: try:
...: datei = open('test.dat')
...: except IOError as e:
...: print('abgefangener Fehler:', e)
...: else:
...: content = datei.readlines()
...: datei.close()
...:
abgefangener Fehler: [Errno 2] No such file or directory: 'test.dat'
Bei der ersten Eingabe ist die Datei test.dat
vorhanden und kann geöffnet
werden. Der except
-Block wird daher übersprungen und der else
-Block
ausgeführt. Im Prinzip hätte man den Inhalt des else
-Blocks auch im
try
-Block unterbringen können. Die hier gezeigte Variante hat jedoch den
Vorteil, dass der Zusammenhang zwischen dem Versuch, eine Datei zu öffnen, und
der eventuellen IOError
-Ausnahme eindeutig ist. In der Eingabe 3 ist die
Datei test.dat
nicht vorhanden, und es wird der except
-Block
ausgeführt. Die Variable e
nach dem Schlüsselwort as
enthält dabei
Informationen, die beim Auslösen der Ausnahme übergeben wurden und hier im
except
-Block zur Information des Benutzers ausgegeben werden. Der
else
-Block wird hier im Gegensatz zum ersten Fall nicht ausgeführt.
Wenn die Ausführung des Codes im try
-Block potentiell zu verschiedenen Ausnahmen
führen kann, ist es sinnvoll, mehrere except
-Blöcke vorzusehen, wie das folgende
Beispiel zeigt.
1 2 3 4 5 6 7 8 9 10 11 12 | def myfunc(x):
mydict = {1: 'eins', 2: 'zwei'}
try:
print(mydict[int(x)])
except KeyError as e:
print('KeyError:', e)
except TypeError as e:
print('TypeError:', e)
myfunc(1.5)
myfunc(5.5)
myfunc(1+3j)
|
Während der Funktionsaufruf in Zeile 10 keine Ausnahme auslöst, führen die
Aufrufe in den Zeilen 11 und 12 zu einem KeyError
, da es den Schlüssel 5
in mydict
nicht gibt, bzw. zu einem TypeError
weil sich die komplexe
Zahl 1+3j
nicht in einen Integer umwandeln lässt. Die Ausgabe sieht
dementsprechend folgendermaßen aus:
eins
KeyError: 5
TypeError: can't convert complex to int
In diesem Beispiel wird Code wiederholt. Dies lässt sich verhindern, wenn man die Funktion wie folgt definiert.
1 2 3 4 5 6 | def myfunc(x):
mydict = {1: 'eins', 2: 'zwei'}
try:
print(mydict[int(x)])
except (KeyError, TypeError) as e:
print(": ".join([type(e).__name__, str(e)]))
|
Möchte man alle Ausnahmen abfangen, so kann man das Tupel in Zeile 5 zum
Beispiel durch Exception
oder gar BaseException
ersetzen. Auf den
Hintergrund hierfür kommen wir etwas später noch zurück.
Einer Folge von except
-Blöcken könnte sich natürlich auch wieder ein
else
-Block anschließen, wie wir dies weiter oben gesehen hatten.
Allerdings gibt es nicht nur Situationen, wo abhängig vom Auftreten einer
Ausnahme der eine oder andere Block abgearbeitet wird, sondern es kann
auch vorkommen, dass am Ende auf jeden Fall ein gewisser Codeblock ausgeführt
werden soll. Ein typischer Fall ist das Schreiben in eine Datei. Dabei muss am
Ende sichergestellt werden, dass die Datei geschlossen wird. Hierzu dient
der finally
-Block. Betrachten wir ein Beispiel.
def myfunc(nr, x):
datei = open('test_%i.dat' % nr, 'w')
datei.write('ANFANG\n')
try:
datei.write('%g\n' % (1/x))
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
datei.write('ENDE\n')
datei.close()
for nr, x in enumerate([1.5, 0, 'Test']):
myfunc(nr, x)
In der Funktion myfunc
soll der Kehrwert des Arguments in die Datei
test.dat
geschrieben werden. Es ergibt sich die folgende Ausgabe:
--- test_0.dat ---
ANFANG
0.666667
ENDE
--- test_1.dat ---
ANFANG
ENDE
--- test_2.dat ---
ANFANG
ENDE
Dabei wird im zweiten Fall wegen der Division durch Null kein Kehrwert ausgeben,
während im dritten Fall ein TypeError
auftritt, weil versucht wird, durch
einen String zu dividieren. Diese Ausnahme wird zwar nicht abgefangen, aber
es ist immerhin garantiert, dass die Ausgabedatei ordnungsgemäß geschlossen wird.
Was würde passieren, wenn man das Schließen der Datei nicht in einem finally
-Block
unterbringt, sondern einfach am Ende der Funktion ausführen lässt? Wir modifizieren
unseren Code entsprechend:
1 2 3 4 5 6 7 8 9 10 11 12 | def myfunc(nr, x):
datei = open('test_%i.dat' % nr, 'w')
datei.write('ANFANG\n')
try:
datei.write("%g\n" % (1/x))
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
datei.write('ENDE\n')
datei.close()
for nr, x in enumerate([1.5, 0, 'Test']):
myfunc(nr, x)
|
Nun erhält man die folgenden Dateiinhalte:
--- test_0.dat ---
ANFANG
0.666667
ENDE
--- test_1.dat ---
ANFANG
ENDE
--- test_2.dat ---
ANFANG
Wie man sieht, ist die letzte Datei unvollständig. Die nicht abgefangene
TypeError
-Ausnahme führt zu einem Programmabbruch, der sowohl die
Ausführung des write
-Befehls in Zeile 10 als auch das Schließen der Datei
verhindert. In der ersten Variante des Programms dagegen gehört der
finally
-Block zum try...except
-Block und wird somit auf jeden Fall
ausgeführt. Erst danach führt der TypeError
in diesem Fall zum
Programmabbruch.
Selbst in obigem Beispiel ohne finally
-Block wurde die Datei geschlossen,
da dies spätestens durch das Betriebssystem beim Programmende veranlasst wird.
Es ist aber dennoch kein guter Stil, sich hierauf zu verlassen. Eine Datei
nicht zu schließen, kann in Python Schwierigkeiten bereiten, wenn man die Datei
anschließend wieder zum Lesen öffnen will. Auch wenn man auf eine große Zahl
von Dateien schreiben möchte, kann es zu Problemen kommen, wenn man Dateien
nicht schließt, da die Zahl der offenen Dateien beschränkt ist. In diesem
Zusammenhang gibt es Unterschiede zwischen verschiedenen Implementationen von
Python. In CPython, also der standardmäßig verwendeten, in der
Programmiersprache C implementierten Version von Python, sorgt ein als »garbage
collection« bezeichneter Prozess, also das Einsammeln von (Daten-)Müll, dafür,
dass überflüssige Objekte entfernt werden. Hierbei werden auch Dateien
geschlossen, auf die nicht mehr zugegriffen wird. Allerdings wird dies nicht
durch die Sprachdefinition garantiert. In Jython, einer Python-Implementation
für die Java Virtual Machine, ist dies tatsächlich nicht der Fall.
Python stellt standardmäßig bereits eine große Zahl von Ausnahmen zur Verfügung,
die alle als Unterklassen von einer Basisklasse, der BaseException
abgeleitet
sind. Die folgende Klassenhierarchie der Ausnahmen ist der Python-Dokumentation
entnommen, wo die einzelnen Ausnahmen auch genauer beschrieben sind. [6]
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StandardError
| +-- BufferError
| +-- ArithmeticError
| | +-- FloatingPointError
| | +-- OverflowError
| | +-- ZeroDivisionError
| +-- AssertionError
| +-- AttributeError
| +-- EnvironmentError
| | +-- IOError
| | +-- OSError
| | +-- WindowsError (Windows)
| | +-- VMSError (VMS)
| +-- EOFError
| +-- ImportError
| +-- LookupError
| | +-- IndexError
| | +-- KeyError
| +-- MemoryError
| +-- NameError
| | +-- UnboundLocalError
| +-- ReferenceError
| +-- RuntimeError
| | +-- NotImplementedError
| +-- SyntaxError
| | +-- IndentationError
| | +-- TabError
| +-- SystemError
| +-- TypeError
| +-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
Will man gleichzeitig Ausnahmen abfangen, die Unterklassen einer gemeinsamen Klasse sind, so kann man stattdessen auch direkt die entsprechende Ausnahmeklasse abfangen. Somit sind beispielsweise
except (IndexError, KeyError) as e:
und
except LookupError as e:
äquivalent. Man kann die vorhandenen Ausnahmeklassen, sofern sie von der Fehlerart her passend sind, auch direkt für eigene Zwecke verwenden oder Unterklassen programmieren. Beim Auslösen einer Ausnahme kann dabei auch eine entsprechende Fehlermeldung mitgegeben werden.
In [1]: raise ValueError('42 ist keine erlaubte Eingabe!')
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-1-c6e93f8997ca> in <module>()
----> 1 raise ValueError("42 ist keine erlaubte Eingabe!")
ValueError: 42 ist keine erlaubte Eingabe!
Hat man eine Ausnahme abgefangen, so hat man die Möglichkeit, nach einer adäquaten Reaktion die Ausnahme erneut auszulösen. Geschieht dies in einer Funktion, so hat das aufrufende Programm wiederum die Möglichkeit, entsprechend zu reagieren. Dies ist im folgenden Beispiel illustriert.
def reciprocal(x):
try:
return 1/x
except ZeroDivisionError:
msg = 'Maybe the main program knows what to do...'
raise ZeroDivisionError(msg)
try:
reciprocal(0)
except ZeroDivisionError as e:
print(e)
print("Let's just continue!")
print("That's the end of the program.")
Dieses Programm erzeugt die folgende Ausgabe:
Maybe the main program knows what to do...
Let's just continue!
That's the end of the program.
Die Funktion reciprocal
fängt die Division durch Null ab. Sie verhindert
damit den vorzeitigen Programmabbruch und gibt dem Hauptprogramm die Chance,
in geeigneter Weise zu reagieren. Dies geschieht hier, indem wiederum der
ZeroDivisionError
abgefangen wird.
Kontext mit with
-Anweisung¶
Im vorigen Abschnitt hatten wir im Zusammenhang mit dem Zugriff auf eine Datei ein typisches Szenario kennengelernt, bei dem die eigentliche Funktionalität zwischen zwei Schritte eingebettet ist, in denen zunächst Vorbereitungen getroffen werden und am Ende notwendige Aufräumarbeiten durchgeführt werden. In unserem Beispiel wäre dies das Öffnen der Datei zu Beginn und das Schließen der Datei am Ende. Eine solche Situation kann in Python mit Hilfe eines Kontextmanagers elegant bewältigt werden. Dies ist im folgenden Beispiel gezeigt.
with open('test.dat', 'w') as file:
for n in range(4, -1, -1):
file.write('{:g}\n'.format(1/n))
Dies entspricht einem try...finally
-Konstrukt, bei dem im finally
-Block
unabhängig vom Auftreten einer Ausnahme die Datei wieder geschlossen wird.
Die Ausgabedatei hat dann den folgenden Inhalt:
0.25
0.333333
0.5
1
Sie wurde explizit beim Verlassen des with
-Blocks geschlossen, nachdem zuvor
die Variable n
den Wert Null erreicht hat und die Division eine
ZeroDivisionError
-Ausnahme ausgelöst hat. Um dies zu überprüfen, muss man die
Ausnahme abfangen.
try:
with open('test.dat', 'w') as file:
for n in range(4, -1, -1):
file.write('{:g}\n'.format(1/n))
except ZeroDivisionError:
print('division by zero')
print('file is closed: {}'.format(file.closed))
Die zugehörige Ausgabe lautet dann wie erwartet:
division by zero
file is closed: True
Kontextmanager können unter anderem beim Arbeiten mit Cython [7]
nützlich sein. Cython ermöglicht die Optimierung von Python-Skripten, indem es
C-Erweiterungen anbietet. Dazu gehört unter anderem die Möglichkeit, den
Datentyp von Variablen festzulegen. In Python werden Listenindizes
normalerweise darauf überprüft, ob sie innerhalb des zulässigen Bereichs liegen,
und es werden negative Indizes entsprechend behandelt. Dies kostet natürlich
Zeit. Ist man sich sicher, dass man weder negative Indizes benutzt noch die
Listengrenzen überschreitet, so kann man bei der Benutzung von Cython auf die
genannte Funktionalität verzichten. Will man dies in einem begrenzten
Code-Block tun, so bietet sich die Verwendung von with
cython.boundscheck(False)
an. Eine andere Anwendung besteht im Ausschalten
des Global Interpreter Locks [8] von Python mit Hilfe des
nogil
-Kontextmanagers.
[1] | Hier verwenden wir %timeit , eine der so genannten magischen
Funktionen der verbesserten Python-Shell IPython , die es erlaubt,
Ausführungszeiten einzelner Befehle zu bestimmen.
Will man die Ausführungszeit eines ganzen Befehlsblocks bestimmen,
so muss die magische %%timeit -Funktion mit zwei Prozentzeichen
verwendet werden. Wir werden hierauf im Abschnitt Das Modul timeit zurückkommen. |
[2] | Für weitere Informationen siehe z.B. den Wikipedia-Eintrag zu double-ended queue. |
[3] | Wir belassen es hier bei dem üblicherweise verwendeten englischen Begriff. Gelegentlich findet man den Begriff »Listenabstraktion« als deutsche Übersetzung. |
[4] | Guido von Rossum begründet das in einem Blog mit dem Titel The fate of reduce() in Python 3000 aus dem Jahr 2005. |
[5] | Wir belassen es auch hier wieder bei dem häufig verwendeten englischen Begriff, der als »Funktionsabschluss« zu übersetzen wäre. |
[6] | Siehe 6. Built-in Exceptions in der Dokumentation der Standardbibliothek von Python. |
[7] | Weitere Informationen zu Cython findet man unter www.cython.org.
Cython sollte nicht mit CPython verwechselt werden, der C-Implementation von Python, die
man standardmäßig beim python -Aufruf verwendet. |
[8] | Siehe das Glossar der Python-Dokumentation für eine kurze Erläuterung des GIL. |