Zapisz jako PDF

Transkrypt

Zapisz jako PDF
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/
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
1/
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 ==
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
assert 1 ==
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 == , 'opis bledu'
Traceback (most recent call last):
File "<pyshell#13>", line 1, in <module>
assert 1 == , '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 = [, 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 = ., y = 1., z = 2.)
B = [, 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', , 1, 2) #KeyError
g('x', 4, 1, 2) #IndexError
g('x', , '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
"Programowanie dla Fizyków Medycznych"