TI/Programowanie dla Fizyków Medycznych:Wyjątki
Wyjątki
Pisząc kod w Pythonie na pewno już nie raz coś poszło nie tak i Shell wypisał komunikat o błędzie podobny do poniższego:
>>> 1/0
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
1/0
ZeroDivisionError: integer division or modulo by zero
Tak na prawdę został zgłoszony pewien wyjątek, gdyż interpreter Pythona spotkał kod, którego nie umiał wykonać (w tym przypadku dzielenie przez 0), następnie interpreter nie znalazł kodu przeznaczonego do obsługi zgłoszonego wyjątku i w efekcie program został przerwany. W tym rozdziale opiszę jak można obsługiwać wyjątki, w jaki sposób po zgłoszeniu wyjątków jest poszukiwany kod do ich obsługi i w końcu jak tworzyć i zgłaszać własne wyjątki.
Python dostarcza wielu wyjątków:
>>> import exceptions
>>> dir(exceptions)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BufferError', 'BytesWarning', 'DeprecationWarning',
'EOFError', 'EnvironmentError', 'Exception', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
'ImportWarning', 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError',
'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning',
'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TypeError',
'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning',
'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '__doc__', '__name__', '__package__']
Warto spróbować wygenerować ręcznie każdy z nich. Na przykład AssertionError jest zgłaszany gdy wyrażenie w poleceniu assert zwróci fałsz:
>>> assert 1 == 0
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
assert 1 == 0
AssertionError
Polecenie assert przydaje się do sprawdzania warunków poprawności w naszym kodzie, jeśli w danym miejscu kodu chcemy się upewnić, że pewien warunek jest spełniony to warto użyć polecenia assert. Do polecenia assert można dodać opis warunku - ten opis zostanie przekazany w wyjątku jeśli asercja będzie fałszywa:
>>> assert 1 == 0, 'opis bledu'
Traceback (most recent call last):
File "<pyshell#13>", line 1, in <module>
assert 1 == 0, 'opis bledu'
AssertionError: opis bledu
Wygoda polecenia assert polega na tym, że wszystkie testy można wyłączyć uruchamiając Pythona z opcją -O w linii poleceń. Poniżej klika przykładów, które zgłaszają wyjątki:
>>> open('plik_ktorego_brakuje', 'r')
Traceback (most recent call last):
File "<pyshell#25>", line 1, in <module>
open('plik_ktorego_brakuje', 'r')
IOError: [Errno 2] No such file or directory: 'plik_ktorego_brakuje'
>>> A = [0, 1, 2]
>>> A[3]
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
A[3]
IndexError: list index out of range
>>> A['Ala']
Traceback (most recent call last):
File "<pyshell#29>", line 1, in <module>
A['Ala']
TypeError: list indices must be integers, not str
>>> import brakujacy_modul
Traceback (most recent call last):
File "<pyshell#36>", line 1, in <module>
import brakujacy_modul
ImportError: No module named brakujacy_modul
>>> B = {}
>>> B['Ala']
Traceback (most recent call last):
File "<pyshell#38>", line 1, in <module>
B['Ala']
KeyError: 'Ala'
>>> a
Traceback (most recent call last):
File "<pyshell#39>", line 1, in <module>
a
NameError: name 'a' is not defined
>>> C = []
>>> c = C.__iter__()
>>> c.next()
Traceback (most recent call last):
File "<pyshell#49>", line 1, in <module>
c.next()
StopIteration
>>> C.iter()
Traceback (most recent call last):
File "<pyshell#50>", line 1, in <module>
C.iter()
AttributeError: 'list' object has no attribute 'iter'
>>> if c
SyntaxError: invalid syntax
Jak widać wyjątki nie zawsze są czymś złym - na przykład wyjątek StopIteration jest zgłaszany gdy iterator dotarł do końca sekwencji i ten mechanizm jest wykorzystywany do zatrzymania pętli for.
Wyjątki można obsługiwać dzięki konstrukcji try-except-else-finally. W bloku try piszemy kod, który chcemy wykonać, następnie możemy podać dowolnie dużo bloków except, mogą one obsługiwać wybraną klasę wyjątku (wraz ze wszystkimi podklasami), wiele klas wyjątków i w końcu wszystkie wyjątki, następnie co najwyżej jeden blok else zawierający kod, który zostanie wykonany jeśli w bloku try nie został zgłoszony żaden wyjątek i blok finally, którego kod zostanie wykonany na samym końcu niezależnie od tego czy w bloku try lub else pojawił się wyjątek czy nie. Blok finally jest zazwyczaj wykorzystywany do wykonywania operacji sprzątania - zamykania otwartych plików, zwalniania połączeń z bazą danych itp. Po wystąpieniu wyjątku przerywane jest normalne wykonanie programu i rozpoczyna się poszukiwanie kodu, który może obsłużyć dany wyjątek - jeśli wyjątek pojawił się w bloku try, to w pierwszej kolejności przeglądane są wszystkie występujące niżej bloki except, jeśli nie zostanie znaleziony odpowiedni, przeszukiwanie przenosi się do miejsc z którego został wywołany kod, który zgłosił wyjątek (jeśli wyjątek pojawił się w funkcji to przechodzimy do kodu wywołującego funkcję), jeśli to wywołanie było otoczone blokiem try to przeszukiwane są odpowiadające mu bloki except itd aż dojdziemy do poziomu skryptu, jeśli nie został znaleziony odpowiedni blok except to wyjątek przerywa wykonanie programu i zostaje wypisany na standardowe wyjście błędów. Szukanie kodu obsługującego dany wyjątek jest przerywane jeśli udało się znaleźć odpowiedni blok except (wyłapujący wyjątki tej klasy co zgłoszony, klas po których zgłoszony dziedziczył lub wszystkie wyjątki), z takiego bloku za pomocą polecenia raise można ponownie zgłosić ten sam wyjątek i znów rozpocznie się poszukiwanie kodu, który może go obsłużyć. Po znalezieniu odpowiedniego bloku except program jest wykonywany dalej od miejsca w którym kończy się odpowiadający mu blok try. Te mechanizmy ilustruje poniższy przykład:
def f(a, b, c, d):
A = dict(x = 0., y = 1., z = 2.)
B = [0, 1, 2]
print 'w funkcji f przed obliczeniami'
try:
wynik = A[a] / B[b] + float(c)
except KeyError:
print 'obsługa wyjątku KeyError w f i ponowne zgłoszenie'
raise
except ArithmeticError:
print 'obsługa wyjątku ArthmeticError w funkcji f'
print 'ale też wszystkich klas dziedziczących po nim, więc w szczególności ZeroDivisionError'
#raise
except ZeroDivisionError:
print 'obsługa ZeroDivisionError w f' #tu nigdy nie trafimy bo wyjątki tego typu są obsłużone poprzednim except
else:
print 'obliczenie się powiodło wynik = ' + str(wynik)
print 'w funkcji f po obliczeniach próbuję otworzyć plik'
try:
p = open(d, 'r')
assert p.read() == '', 'plik nie jest pusty'
except IOError:
print 'obsługa wyjątku IOError w funkcji f'
else:
print 'blok else w funkcji f'
finally:
print 'blok finally w funkcji f'
if 'p' in dir():
p.close()
print 'kod po bloku try w f'
def g(*a, **b):
try:
print 'przed wywołaniem f'
f(*a, **b)
print 'po wywołaniu f'
except TypeError as error:
print 'obsługa wyjątku TypeError w funkcji g, wypiszę informacje o wyjątku:'
print 'typ wyjątku :' + str(type(error)) + ' dołączona infomracja : ' + str(error)
except (KeyError, AssertionError):
print 'obsługa wyjątku KeyError i AssertionError w funkcji g'
else:
print 'blok else w funkcji g'
finally:
print 'blok finally w funkcji g'
print 'kod po bloku try w g'
Różne zachowania powyższego kodu można zobaczyć uruchamiając kolejno następujące wywołania (w komentarzach podano wyjątki, które są zgłaszane):
g() #TypeError
g('a', 0, 1, 2) #KeyError
g('x', 4, 1, 2) #IndexError
g('x', 0, '2.2', 'pusty_plik') #ZeroDivisionError
try:
g('x', 1, 'a', 'pusty_plik') #ValueError
except:
print 'nie podając klas wyjątków jakie chce złapać - łapię wszstkie'
g('x', 1, '2.2', 'b') #IOError
g('x', 1, '2.2', 'niepusty_plik') #AssertionError
g('x', 1, '2.2', 'pusty_plik') #brak wyjątków
Warto podkreślić, że ponowne zgłoszenie wyjątku w bloku except poszukiwania bloków obsługujących dany wyjątek omija bloki poniżej aktualnie wykonywanego except (po odkomentowaniu raise w except ArthmeticError w funkcji f nadal nie wchodzimy do except ZeroDivisionError).
Wyjątki można stosować zamiast przeprowadzania testów danych które dostaliśmy - na przykład zamiast sprawdzać czy użytkownik wpisał na wejściu liczbę całkowitą po prostu wykonajmy rzutowanie otrzymanego napisu na int i jeśli coś się nie powiodło to zostanie zgłoszony wyjątek. Takie podejście zwiększa czytelność kodu. Pisząc testy musimy w wielu miejscach wstawić konstrukcję warunkową, natomiast wykorzystując wyjątki cały kod, który może się nie udać wstawiamy do bloku try, a później piszemy odpowiednie bloki except.
Tworzone samodzielnie wyjątki powinny być klasami dziedziczącymi po Exception, można je zgłaszać poleceniem raise, oto prosty przykład:
class MyException(Exception):
def __init__(self, info):
self.info = info
def __str__(self):
return str(self.info)
try:
raise MyException("informacja niesiona z wyjątkiem")
except MyException as error:
print error