basics michael poeltl © 2011,2013 |
Das Wort Exception kommt aus dem Englischen und bedeutet Ausnahme.
Oft hat das in python mit error-handling zu tun. Wie reagieren, wenn dieser oder jener Fehler auftritt?
Irgendwo werden wir das vielleicht schon einmal gemacht haben, dass wir etwas auf gewünscht/unerwünscht abfragen. Zum Beispiel, wieviele Parameter wurden dem script übergeben?
Weniger als zwei Parameter ist unerwünscht.
if len(sys.argv) < 3:
und wenn das eintritt, dann soll das Programm mit einer Fehlermeldung abgebrochen werden.
print ("Zuwenige parameter", file=sys.stderr); sys.exit(1)
Mit sys.exit(2) wurde der ProgrammStop erzwungen, sobald weniger als zwei Parameter dem script übergeben wurden.
Unsere Programme laufen fehlerfrei ab, Fehler oder unerbetenes sowie unerwartetes Verhalten bilden die AUSNAHME (Excetion).
Exception handling meint dann jenen Bereich (Codeblock), wo der Code steht, der auf so eine (unerwartete/unerwünschte) exception reagiert.
Nach einer Eingewöhnungsphase ist es heute völlig normal für mich, mit Exceptions (Ausnahmen) zu arbeiten. Warum das so gekommen ist, versuche ich Dir hier zu zeigen.
Exceptions helfen der Programmiererin, auf ungewöhnliches, unvorhergesehenes oder unerwünschtes im Programmablauf adäquat zu reagieren. Zumeist handelt es sich um error-handling, aber nicht ausschließlich.
python hat eine Menge unterschiedlicher Exceptons, und die Programmiererin kann auch ihre eigenen Exceptions definieren.
Wenn ich dir sage, daas dir bereits Exceptions begegnet sind, wirst du wahrscheinlich ungläubig den Kopf schütteln.
>>> a = 3 + '2.1' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'int' and 'str' >>> b = 5/0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: division by zero >>>
TypeError
und ZeroDivisionError
sind Exceptions. Und von denen gibt es noch einen ganzen Haufen.
>>> for e in dir(__builtins__): ... if 'Error' in e: ... print (e) ... ArithmeticError AssertionError AttributeError BufferError EOFError ... IndexError KeyError ... UnicodeTranslateError ValueError ZeroDivisionError >>>
Könnten wir OHNE error-handling überhaupt auskommen? Sicher nicht, oder?
Oder reichte es nicht, anhand der Rückgabewerte von Funktionen Maßnahmen zu setzen, die dann darauf reagieren sollen? (Das haben wir in C.). Der error-handling-code schaut dann meist wie ein Stopfwerk aus, und wenn man einen Error zurückbekommt mit einem Error-Code z.B.: 127), dann weiss man nicht gleich, was das bedeutet. Auf mich wirkt das doch ziemlich kompliziert.
Wenn man das mit Exceptions vergleicht, dann kommt man drauf, dass man mit Exceptions einen um Längen besser verständlchen code schreiben kann. Das error-handling hier funktioniert viel einfacher!
Eine Exception sieht in der Regel so aus:
try: #CODE in diesem Block ausfuehren #'try' gibt an, dass die Ausfuehrung versucht wird #wenn Problem -> raise/throw exception except: #wenn eine exception auftritt, #(was nicht immer ein error sein muss) #dann fuehre code in diesem Block aus. #== catching exception
Wenn der code eine Exception verursacht, dann redet man im Fachjargon von raising an exception
oder von throwing an exception
.
Die Antwort auf eine Exception wird catching an exception
, und der code, der auf so eine exception angesetzt wird heißt exception-handling code
oder kurz exception handler
.
Konkretes Beispiel:
>>> def zaehle_zeilen(file): ... ''' ... wenn file existiert, dann gib die Anzahl der Zeilen ... zurueck. ... Wenn file nicht existiert, dann gib 0 zurueck ... ''' ... try: ... return len(open(file).readlines()) ... except: #'irgendwas' lief schief und #Zeilenanzahl konnte nicht berechnet werden ... return 0 ... >>>
Sollte der try-Block (irgend-)eine exception raisen, wird sie von except abgefangen und der except-Block ausgeführt..
Wir wissen aber, dass es eine ganze Menge von exceptions gibt (IndentationError, NameError, SystemExit, KeyboardInterrupt etc). aber wozu, wenn ich ohnehin jede exception mittels except abfangen kann?
Aber das widerspricht einem der Punkte/Prinzipien im The Zen of Python
, wo zu lesen ist:
Errors should never pass silently.
Durch bloßen Einsatz von except: verliert man jegliche Feinkörnigkeit und die Fehler würden ganz still und leise abgefangen. Im Prinzip wollen wir ja nur den Fall abfangen
, wenn das file nicht existiert, also einenNo such file or directory
oder wie das immer heißen mag. Schauen wir doch nach, welche exception geraised wird!
>>> f = open('mich_gibts_nicht') Traceback (most recent call last): File "<stdin>", line 1, in <module> IOError: [Errno 2] No such file or directory: 'mich_gibts_nicht' >>>
IOError ist also der Name der exception, die wir in zaehle_zeilen() abfangen sollten - alle anderen sollen nach wie vor laut krachen
.
>>> def zaehle_zeilen(file): ... ''' ... wenn file existiert, dann gib die Anzahl der Zeilen ... zurueck. ... Wenn file nicht existiert, dann gib 0 zurueck ... ''' ... try: ... return len(open(file).readlines()) ... except IOError: ... #irgendwas ist mit dem file (gibt's nicht, ... # kann nicht gelesen werden, etc) ... return 0 ... >>>
Jetzt wird gezielt ein Fehler abgefangen, und alle anderen, die auftreten können, werden nicht abgefangen, somit erfüllen wir wieder die Zen-Vorgabe.
In python ist eine Exception ein Objekt. Jedes Objekt hat einen Typ und eine object-id:
>>> print (type(IOError())) <class 'IOError'> >>> print (id(IOError)) 136062048 >>>
IOError und OSError sind beides subclasses von EnvironError.
wie könnte man da draufkommen?
>>> help(OSError) Help on class OSError in module builtins: class OSError(EnvironmentError) >>> help(IOError)
EnvironError hat nur diese beiden als subclass --<
try: blabla except EnvironmentError: blabla
bedeutet, dass sowohl IOError als auch OSError durch diese eine Ausnahme abgefangen werden, andere (TypeError) aber nicht!
try: blabla except (EnvironError, TypeError): blabla
Und das wiederum umgelegt auf das vorige beispel
>>> try: ... f = open('mich_gibts_nicht') ... except (EnvironmentError, TypeError): ... print ("Fehler") ... Fehler >>>
Hier wurde nun gezeigt, dass man mehrere Exceptions zusammenfassen kann. Wenn eine auftritt, dann wird die exception abgefangen. Im obigen Fall waren das IOError, OSError und TypeError.
Es ist dann aber schwer bis unmöglich, die drei Exceptions wiederzuerkennen. Welcher Fehler war es denn?
Man kann für jede Exception eine eigene except-clause bauen. Die Struktur sieht dann so aus:
try: 'code der eine exception hervorrufen koennte' except TypeError: 'code der TypeError *handled*' except IOError: 'code der IOError-Exception handled'
Und das führt mich gleich zur ganzen Syntax vom try-exception-Konstrukt, und das sieht so aus:
try: code except TypeError: blabla except IOError: blabla except OSError: blabla else: code, der ausgefuehrt wird, wenn code im try-Block KEINE exception geraised hat finally: code der in jedem Fall zuletzt (nach all den excepts oder dem else) ausgefuehrt wird man nennt diesen Bereich auch "cleanup"-code (Aufraeum-code), denn hier koennen offene file-Objekte geschlossen werden oder identifier dereferenced werden
Konstruieren wir uns zwei Beispiele: Einmal wo ein import eines Moduls eine exception raised und das andere mal, wo es funktioniert.
>>> try: ... import bambam ... except (ImportError) as e: ... print (e) ... else: ... print ('Erfolgreich Modul importiert') ... finally: ... print ('das beste kommt zum Schluss') ... No module named bambam das beste kommt zum Schluss >>> try: ... import sys ... except (ImportError) as e: ... print (e) ... else: ... print ('Erfolgreich Modul importiert') ... finally: ... print ('das beste kommt zum Schluss') ... Erfolgreich Modul importiert das beste kommt zum Schluss >>>
Nun, das war ganz klar - gut lesbar, und es hängt wie eine Traube im try-Block und somit ist allzeit auf einem Blick klar, was zusammengeh¨rt.
Ist dir das as e
aufgefallen?
Jede exception hat Argumente, die es beim Aufruf übergeben bekommt.
>>> import blabla Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: No module named blabla >>>
In diesem Fall wurde No module named blabla
der ImportError()-Klasse als Argumet übergeben.
Wir können übrigens auch bewusst eine exception mithilfe der raise-Funktion hervorrufen.
>>> try: ... raise ImportError('Des Modul gibts net') ... except (ImportError) as fehler: ... print (fehler) ... Des Modul gibts net >>>
Und so könnten wir auch eine python-Interpreter-session auf der commandline beenden:
>>> raise(SystemExit('byebye')) byebye ?> echo $? 1 ?>
Das beendet also nicht nur die session, sondern die Meldung byebye kam auch über'n Fehlerkanal (2 => stderr).
Manchmal möchte man seine eigene Excepton, mit eigenem Namen einzetzen.
Und das geht wirklich sehr einfach.
Man erbt einfach vn der Exception-klasse und..., ah - see for yourself.
>>> class WetterVorhersageError(Exception): ... pass ... >>> try: ... raise WetterVorhersageError('Falsche Vorhersage') ... except (WetterVorhersageError) as e: ... print (e) ... Falsche Vorhersage >>>
Wenn wir also ein Projekt hätten, das mit Wettervorhersage zu tun hat, wäre das doch eine passende Exception gewesen.
Mithilfe von assert() kann man leicht eine Art debug-Code in python platzieren. Den kann man dann OHNE speed-loss im python-Code lassen.
Das funktioniert aber NUR, wenn __debug__
auf True gesetzt ist. __debug__ist per default auf True gesetzt. Wie kann man das auf False Setzen
?
Zum einen, kann man das pythonscrpt mit der optimize-Option (-O, -OO) aufrufen (-O --> großes o), zum anderen kann man eine Shellvariable setzen:
PYTHONOPTIMIZE=-O
So, check this out.
?> echo $PYTHONOPTIMIZE -O ?> python3 Python 3.2 (r32:88445, Feb 21 2011, 04:07:45) [GCC 4.4.3] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> print (__debug__) False >>> exit() ?> unset PYTHONOPTIMIZE ?> python3 ... >>> print (__debug__) True >>>
assert() wird also völlig ignoriert, wenn __debug__ auf False gesetzt itst.
Wenn __debug__ ON ist (True), und eine assert()-Zeile greift, dann wird eine AssertionError geraised, die man wiederum mittels eines exception-handlers verarbeiten kann.
Die Syntax von assert() sieht so aus:
assert AUSDRUCK, args
>>> def as_check(n): ... a = 9 ... for i in range(n): ... assert i<a, '{} zu gross'.format(i) ... >>>
Mein debug-code checkt, ob der Wert niemals groessergleich als 9 ist; wenn's aber doch passiert, dann soll der AssertionError auftreten.
In code-Pasagen, wo Werte u.U. schwrer vorhersagbar sind, es aber klar ist, welche Werte unerwünscht sind, genau dafür passt diese Art des debuggens.
Debug-Code ist DEAKTIV, wenn __debug__ auf False, und leicht aktiviert, wenn __debug__ auf True.
>>> try: ... as_check(11) ... except (AssertionError) as e: ... print (e) ... 9 zu groß >>>