Podstawy wyjątków

https://docs.python.org/3/library/exceptions.html

https://docs.python.org/3/reference/executionmodel.html#exceptions

PEP 765 - Disallow return/break/continue that exit a finally block

WPROWADZENIE

Przy próbie uruchomienia kodu Pythona mogą pojawić się co najmniej dwa rodzaje błędów: błędy składniowe (syntax errors) oraz błędy czasu wykonania (runtime errors). Oba rodzaje błędów są obsługiwane przez wyjątki (exceptions). Z drugiej strony, pojawienie się wyjątku nie zawsze oznacza błąd w programie.

BŁĘDY SKŁADNIOWE

Błędy składniowe, inaczej błędy parsowania, wywołane są przez nieprawidłową składnię instrukcji. W efekcie otrzymujemy komunikat z numerem linii z błędem i jej treścią. Mała strzałka pokazuje token przed którym wykryto błąd.


>>> if True print("word")
  File "<stdin>", line 1
    if True print("word")   # brak dwukropka PRZED print() (Py3.13)
            ^^^^^
SyntaxError: invalid syntax

Jeżeli instrukcja jest poprawna składniowo, to w dalszym ciągu mogą się pojawić błędy przy próbie jej uruchomienia (runtime errors). Takie błędy są nazywane wyjątkami i niekoniecznie muszą oznaczać katastrofę.

WYJĄTKI

Wyjątek jest to zdarzenie, które może modyfikować przebieg sterowania w programów. W Pythonie wyjątki wywoływane są automatycznie w momencie wystąpienia błędów i mogą być wywoływane oraz przechwytywane przez nasz kod.

Wyjątki pozwalają nam wyskoczyć z dowolnie dużych części programu do kodu z programu obsługi wyjątku. Jest to spójny sposób reagowania na niezwykłe zdarzenia, narzędzie sterowania wysokiego poziomu.

Wyjątki są przetwarzane przez cztery instrukcje:

Najważniejsze powody wykorzystywania wyjątków:

Kiedy pojawi się błąd w czasie wykonywania programu, tworzony jest wyjątek. Zwykle wtedy program jest zatrzymywany, a Python wypisuje komunikat o błędzie. Tak działa domyślny program obsługi wyjątków. Standardowo komunikat o błędzie obejmuje zgłoszony wyjątek wraz ze śladem stosu, czyli listą wszystkich wierszy oraz funkcji aktywnych w momencie, kiedy nastąpił wyjątek.


# Przykłady poleceń powodujących wyjątki.
# Dzielenie przez zero - ZeroDivisionError.
print(23/0)

# Odwołanie się do nieistniejącego elementu listy - IndexError.
alist = []                        # pusta lista
print(alist[5])

# Odwołanie się do nieistniejącego klucza w słowniku - KeyError.
adict = {}                        # pusty słownik
print(adict["key"])

# Otwarcie do czytania nieistniejącego pliku - IOError.
afile = open("nie_istnieje.txt", "r")

PRZECHWYTYWANIE WYJĄTKÓW

Czasem nie chcemy, aby program zatrzymał się po wystąpieniu wyjątku. Wtedy należy opakować wywołanie w instrukcję try/except/else w celu samodzielnego przechwycenia wyjątku. Jeżeli zależy nam na wykonaniu pewnych działań końcowych, niezależnych od wystąpienia wyjątku, to stosujemy instrukcję try/finally.

Od Pythona 2.5 mamy jedną instrukcję try/except/finally, czyli bloki except i finally mogą wystąpić w jednej instukcji try.


L = []
# L = [1, 2, 3]
try:
    print(L[1])
except IndexError:            # przechwycenie wyjątku
    print("mam wyjątek")   # nasz program obsługi wyjątku
else:
    print("nie było wyjątku")
print("kontynuuję")

Jeżeli podczas wykonywania bloku try nie wystąpił wyjątek, to następnie będzie wykonany blok else, a potem instrukcje poza instrukcją try/except.
Jeżeli podczas wykonywania bloku try wystąpił wyjątek IndexError, to następnie będzie wykonany blok except IndexError, a potem instrukcje poza instrukcją try/except.
Jeżeli podczas wykonywania bloku try wystąpił wyjątek inny niż IndexError, to wyjątek będzie przekazany wyżej (szczegóły zależą od położenia instrukcji try/except).


L = []
# L = [1, 2, 3]
try:
    print(L[1])
finally:                      # działania końcowe
    print("zawsze wykonane")
print("kontynuuję")

Jeżeli podczas wykonywania bloku try nie wystąpił wyjątek, to następnie będzie wykonany blok finally, a potem instrukcje poza instrukcją try/finally.
Jeżeli podczas wykonywania bloku try wystąpił wyjątek, to następnie będzie wykonany blok finally, ale potem wyjątek będzie przekazany wyżej (szczegóły zależą od położenia instrukcji try/finally).

Po otwarciu pliku do czytania również mogą się pojawić różne błędy, które przerwą program. Można zastosować konstrukcję try/finally, aby na pewno zamknąć otwarty plik. Obecnie zaleca się korzystanie z menedżera kontekstu (instrukcja with).


afile = open("tekstowy.txt")  # najpierw otwieramy plik tekstowy
try:                          # pracujemy na pliku
    text = afile.read()   # czytamy cały tekst
finally:                      # na pewno zamkniemy plik
    afile.close()

Sprawdzenie poprawności nazwy pliku podanej przez użytkownika można wykonać przy pomocy wyjątków.


# Bezpieczna obsługa pliku.
#filename = raw_input("Podaj nazwę pliku: ")   # Py2
filename = input("Podaj nazwę pliku: ")   # Py3
try:                          # pierwszy blok
    afile = open(filename, "r")
except Exception:             # drugi blok (w razie wyjątku)
    print("Nie ma pliku o nazwie {}".format(filename))
else:                         # trzeci blok opcjonalny (nie było wyjątku)
    print("Plik został otwarty")

Można zamknąć taką funkcjonalność wewnątrz funkcji.


# Funkcja zwraca True, jeżeli plik istnieje, lub False, w przeciwnym razie.
def if_exist(filename):
    try:
        afile = open(filename)
        afile.close()
        return True
    except Exception:
        return False

Ogólny format instrukcji try/except/else/finally zawiera wiele opcjonalnych bloków z programami obsługi, choć musi pojawić się przynajmniej jeden.


# Składnia.
try:
    instrukcje            # podstawowe działanie instrukcji
except ExceptionClass1:   # przechwytuje wskazany wyjątek
    instrukcje
except (ExceptionClass2, ExceptionClass3): # przechwytuje wymienione wyjątki
    instrukcje
except ExceptionClass4 as exception1: # przechwytuje wyjątek i jego instancję
    instrukcje
except (ExceptionClass5, ExceptionClass6) as exception2: # przechwytuje wyjątki i instancję
    instrukcje
except:                  # przechwytuje wszystkie (pozostałe) wyjątki
    instrukcje
else:                    # działania przy braku zgłoszenia wyjątku
    instrukcje
finally:                 # działania końcowe
    instrukcje

Należy ostrożnie korzystać z pustej części except, ponieważ może przechwycić nieoczekiwane wyjątki systemowe niezwiązane z naszym kodem albo wyjątki przeznaczone dla innych programów obsługi. Lepsza jest postać except Exception, która ignoruje wyjątki powiązane z systemowymi wyjściami z programu.

Część else jest pomocna przy ustaleniu, czy wyjątek nie został zgłoszony, czy wystąpił i został obsłużony.

PEP 765 zawiera uzasadnienie, dlaczego w części finally Python nie pozwala na użycie instrukcji return, break i continue. Dzięki temu zapobiega się wielu błędom trudnym do wykrycia, a także poprawia się bezpieczeństwo.

ZGŁASZANIE WYJĄTKÓW

Do ręcznego wywoływania wyjątków służy instrukcja raise.


try:
    raise IndexError   # rzucamy wyjątek IndexError
except IndexError:            # przechwycenie wyjątku
    print("mam wyjątek")   # nasz program obsługi wyjątku
print("kontynuuję")

>>> raise IndexError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError

>>> raise IndexError, "message"   # stara składnia Py2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: message

>>> raise IndexError("message")   # nowa składnia Py2.7, Py3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: message

Do wywołania wyjątku można także wykorzystać wyrażenie assert, które jest wykorzystywana głównie przy debugowaniu kodu. Jest to pomoc dla programistów do wyszukiwania błędów w programie, sprawdza się występowanie "niemożliwych" sytuacji. Nie jest to mechanizm do obsługi błędów czasu wykonania (run-time errors). AssertionError nie powinien się nigdy pojawić, jeżeli program nie ma błędów.


>>> assert False, "message"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: message