O Obsłudze Wyjątków i Wyjątkach Słów Kilka

  • by

Jeden powiedziałby „jak ja nie cierpię tych wyjątków!” Inny rzekłby „wszystko by mi się udało, gdyby nie te wścibskie wyjątki i ten ich głupi traceback”. Aby zrozumieć, czym są wyjątki, dlaczego się pojawiają oraz jak je obsłużyć zagłębmy się w podstawach i omówmy wszystko, co jest z nimi związane.

Definicja

Wyjątek to mechanizm do obsługi niespodziewanych zdarzeń. Generowany jest, gdy pojawi się sytuacja nieprzewidziana oraz nieobsłużona przez programistę. Język Python ma zbiór wyjątków, które są przez niego zwracane w sytuacjach, które w świetle kodu nie powinny mieć miejsca. Ale wszystko po kolei.

Aby zobaczyć, co się dzieje, gdy zrobimy coś niespodziewanego, spróbujmy wygenerować błąd. Najprościej i najszybciej możemy to zrobić, dzieląc przez zero!

1/0
Traceback (most recent call last):
  File "/home/jarek/Playground/blog_entry/exception.py", line 1, in <module>
    1/0
ZeroDivisionError: division by zero

Generuje to tzw. traceback, czyli informacje z ostatniej transkacji – gdzie, i jaki wyjątek wystąpił. W tym przypadku wystąpiłZeroDivisionError. Czy to jedyny wyjątek w Pythonie?

Drzewo wyjątków

Python w kwestii wyjątków korzysta z dziedziczenia. Głównym rodzicem wszystkich wyjątków jest BaseException, po którym dziedziczą dwa kolejne wyjątki – Exception który jest bazowym wyjątkiem wszystkich wyjątków, z których korzystamy w kodzie, oraz KeyboardInterrupt który pomaga obsługiwać wszelkie skróty klawiszowe zatrzymujące pracę skryptu. Z tego też względu Nie możemy korzystać przy obsłudze wyjątków z wyjątku BaseException, ponieważ użycie skrótów klawiszowych mogłoby być zignorowane.

Drzewo wyjątków - obsługa wyjątków
Drzewo wyjątków

Wracając do Exception. Na powyższym obrazku możemy zauważyć, że pewne błędy zawierają się w sobie. Przypuśćmy, że nasza funkcja może wygenerować IndexError lub KeyError. Dzięki dziedziczeniu możemy przy obsłudze wyjątków skorzystać z ich rodzica, aby wyłapać obydwa wyjątki. Dość wygodne rozwiązanie.

Obsługa wyjątków

Ale jak obsługiwać wyjątki? Musimy skorzystać z klauzuli try except. Część try służy do zapisu kodu, który może wygenerować wyjątek. Przy instrukcjiexception definiujemy wyjątek, a następnie opisujemy procedurę do wykonania gdyby wyjątek się pojawił. W poniższym przypadku gdy wystąpi wyjątek, wyświetlimy użytkownikowi wiadomość, że wyjątek miał miejsce.

try:
    1/0
except ZeroDivisionError:
    print("Exception occured")
Exception occurred

Except as

Przy obsłudze konkretnego wyjątku możemy także odwołać się do niego, aby wyświetlić informacje o nim, lub odpowiednio go obsłużyć w zależności od jego argumentów. Możemy dzięki temu, chociażby wygenerować odpowiedni komunikat, który zawierać będzie klasę wyjątku oraz jego treść.

try:
    1/0
except ZeroDivisionError as e:
    print(dir(e))
    print(f"Exception {type(e).__name__}({e}) occurred")
['__cause__', '__class__', '__context__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__suppress_context__', '__traceback__', 'args', 'with_traceback']
Exception ZeroDivisionError(division by zero) occurred

Else

Czasem może się okazać, że kod, który miał wywołać wyjątek, nie wywołał go. Gdy potrzebujemy obsłużyć taki przypadek, stosujemy dodatkowo else, w którym kod zostanie wywołany, gdy żaden wyjątek się nie pojawi. Dla przykładu gdy poprawimy nasz try na poprawny kod, wyjątek się nie pojawi, więc otrzymamy komunikat z sekcji else.

try:
    1/2
except ZeroDivisionError:
    print("Exception occurred")
else:
    print("No exception")
No exception

Finally

Nareszcie! Gdy wykonamy operację oraz obsłużymy wyjątki, oraz sytuacje jego braku, możemy zakończyć obsługę. Gdyby jednak okazało się, że potrzebujemy coś wykonać zawsze po tej obsłudze, skorzystać można z finally. Kod w tej sekcji zostanie uruchomiony zawsze na zakończenie, nie można od jego egzekucji uciec. Przydaje się to gdy musimy zamknąć jakieś źródło, albo przesłać pewien log.

try:
    1/0
except ZeroDivisionError:
    print("Exception occurred")
else:
    print("No exception")
finally:
    print("Exit")
Exception occurred
Exit

Obsługa wielu wyjątków

Mówiliśmy sobie o drzewie wyjątków. Nasza funkcja w zależności od przekazanych argumentów może generować całą listę wyjątków. Nic nie stoi na szczęście na przeszkodzie, aby każdy z nich odpowiednio obsłużyć! A więc, w try except możemy obsługiwać wiele wyjątków poprzez dodanie kolejnych sekcji except z kolejnym wyjątkiem do obsłużenia. Poniżej jeden z przykładów, gdy oczekiwane argumenty funkcji okazały się być innego typu niż int.

try:
    None/None
except ZeroDivisionError as e:
    print(f"Exception {type(e)}")
except TypeError as e:
    print(f"Exception {type(e)}")
Exception <class 'TypeError'>

Ponieważ powyższy przypadek obsługuje wyjątki w ten sam sposób, możemy także połączyć je. Z pomocą przychodzi tupla wyjątków, dzięki której pod jednym except możemy obsłużyć kilka wyjątków jedną logiką:

try:
    None/None
except (ZeroDivisionError, TypeError) as e:
    print(f"Exception {type(e)}")
Exception <class 'TypeError'>

Kolejność obsługi

Pamiętajmy jednak o jednym. Kolejność ma zawsze znaczenie. Tak samo jak w if elif else kolejność wyrażeń logicznych jest ważna. Jeżeli wyżej przekażemy wyjątek wyższego rzędu, to on wyłapie wyjątek jako pierwszy, przed konkretnymi sekcjami oczekującymi na ten konkretny wyjątek. W tym wypadku nasz wyjątek zostanie wyłapany przez rodzica wszystkich wyjątków, czyli Exception.

try:
    1/0
except Exception as e:
    print(f"Catch all exceptions {type(e)}")
except ZeroDivisionError as e:
    print(f"Exception {type(e)}")
except TypeError as e:
    print(f"Exception {type(e)}")
Catch all exceptions <class 'ZeroDivisionError'>

Wywołanie wyjątków

Tworząc nasz kod, nie zawsze możemy uniknąć wszystkich sytuacji. Czasem wręcz nie chcemy, aby użytkownik miał możliwość wykonać jakąś akcję. Aby okrzyczeć użytkownika, w stylu programisty Python, używamy instrukcji raise. Pozwala ona wyrzucić wyjątek użytkownikowi.

print("Some code")
raise LookupError("This code is dangerous!")
Some code
Traceback (most recent call last):
  File "/home/jarek/Playground/blog_entry/exception.py", line 48, in <module>
    raise LookupError("This code is dangerous!")
LookupError: This code is dangerous!

Własny wyjątek

Nie każda sytuacja będzie miała dobry odpowiednik w Python. Są sytuacje gdy dużo lepiej pasowałby nasz własny wyjątek. Na szczęście wyjątki dziedziczą po sobie, a więc i my możemy stworzyć własny wyjątek! Najprostsza jego implementacja to stworzenie klasy dziedzicząca po klasie Exception. Ze względu na to, że klasa musi coś posiadać w swoim ciele, dobrą praktyką jest dodanie docstringów.

class CustomException(Exception):
    """Raised when needed"""

raise CustomException
  File "/home/jarek/Playground/blog_entry/exception.py", line 51, in <module>
    raise CustomException
__main__.CustomException

Klasa bazowa wyjątków

Są pewne biblioteki, które generują masę różnych wyjątków. Głównie te powiązane z API. Aby użytkowników mógł je wszystkie na przykład zignorować, tworzymy dla modułu czy biblioteki wyjątek bazowy. Dzięki temu możemy wyłapywać wszystkie wyjątki danej biblioteki poprzez jeden jej bazowy wyjątek, bez wyłapywania wyjątków, chociażby tych wbudowanych Pythona.

class LibraryException(Exception):
    """Base exception for all exceptions in library"""

class CustomException(LibraryException):
    """Raised when needed"""

try:
    raise CustomException
except LibraryException as e:
    print(f"Library exception handled {type(e)}")
Library exception handled <class '__main__.CustomException'>

Nie zapominajmy też, o obsłudze wyjątków zewnętrznych bibliotek w naszym kodzie. W ich przypadku należy wyjątki uprzednio zaimportować! Zdecydowanie nie powinniśmy ich obsługiwać, poprzez obsługę bazowego Exception.

Pokemon Exception Handling

W ramach wyjątków czuję się odpowiedzialny, aby zwrócić Waszą uwagę na anty pattern związany z obsługą wyjątków. Nazwa pochodzi ze świata pokemon, i dobrze opisuje ten pattern. „Pokemon – gotta catch ’em all”. Zdecydowanie nie powinniśmy przy obsłudze wyłapywać wszystkich wyjątków, a następnie ich ignorować.

try:
    something()
except Exception:
    pass

Jak widać, temat wyjątków jest duży, ale nie ma powodu do obaw. Zapamiętajmy z niego co najważniejsze, czyli jak obsługiwać wyjątki, jak poprawnie tworzyć własne wyjątki oraz w jaki sposób je wyrzucać. Nie ignorujmy tego tematu w naszym kodzie, oraz nie zapominajmy o tej możliwości. Wierzę, że od dziś spojrzycie na wyjątki bardziej przyjacielskim okiem!