basics michael poeltl © 2011,2013

Exceptions

Einleitung

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.

Wir sehen, dass die in Kamelbuckel-Schreibweise gehalten sind. ValueError oder IndentationError und so weiter - wo können wir diese Exceptins aufspüren? Zum Beispiel hier:

>>> 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!

try...except...else...finally

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 einen
No 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.

Die raise()-Funktion

>>> 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).

Eine eigene Exception bauen

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.

assert()

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ß
>>>

Hier geht es zum Seitenanfang und da geht es zur python-Startseite