Skip to content

5. Wyjątki

Zajmiemy się teraz wyjątkami.

Image

5.1. skrypt [exceptions_01]

Pierwszy skrypt ilustruje konieczność obsługi wyjątków.

# celowe wywołanie błędu
x = 4 / 0

Celowo wywołujemy błąd, aby zobaczyć, co się stanie (exceptions-01.py):

1
2
3
4
5
6
7
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_01.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_01.py", line 2, in <module>
    x = 4 / 0
ZeroDivisionError: division by zero

Process finished with exit code 1

Wiersz 4 podaje:

  • typ wyjątku: ZeroDivisionError;
  • powiązany komunikat o błędzie: division by zero. Jest on w języku angielskim. Być może będziemy chcieli to zmienić.

Podstawową zasadą jest, że należy dołożyć wszelkich starań, aby uniknąć wyjątków generowanych przez interpreter języka Python. Musimy sami obsługiwać błędy.

Składnia obsługi wyjątków jest następująca:


try:
    actions susceptibles de lancer une exception
except (Ex1, Ex2…) as ex:
    actions de gestion de l'exception [ex]
except (Ex11, Ex12…) as ex:
    actions de gestion de l'exception [ex]
finally:
   actions toujours exécutées qu'il y ait exception ou non

W bloku try wykonywanie akcji zatrzymuje się, gdy tylko wystąpi wyjątek (błąd). W takim przypadku wykonywanie jest kontynuowane zgodnie z akcjami zawartymi w jednej z klauzul except:

  • wiersz 3: jeśli wystąpił wyjątek [ex], którego typ należy do krotki (Ex1, Ex2…) lub jest od niej pochodny, wówczas wykonywane są działania z wiersza 4;
  • wiersz 5: jeśli wyjątek nie został przechwycony przez wiersz 3 i istnieje inna klauzula [except], wówczas przebiega ten sam proces. Itd…;
  • może istnieć tyle klauzul [except], ile jest konieczne do obsługi różnych typów wyjątków, które mogą wystąpić w [try];
  • jeśli wyjątek nie został obsłużony przez żadną z klauzul [except], zostanie on przekazany do kodu wywołującego. Jeśli ten sam znajduje się w strukturze try / except, wyjątek jest ponownie obsługiwany; w przeciwnym razie jest on przekazywany dalej w górę łańcucha wywoływanych metod. W ostateczności trafia ona do interpretera języka Python. Interpreter ten zatrzymuje wówczas wykonywany program i wyświetla komunikat o błędzie podobny do tego pokazanego w poprzednim przykładzie. Zasada jest zatem taka, że program główny musi przechwycić wszystkie wyjątki, które mogą zostać przekazane z wywoływanych metod;
  • wiersz 7: klauzula [finally] jest zawsze wykonywana, niezależnie od tego, czy wystąpił wyjątek (kontynuacja bloku except**), czy nie (kontynuacja bloku try**). Dzieje się tak nawet wtedy, gdy wyjątek wystąpił, ale nie został przechwycony. W takim przypadku klauzula [finally] zostanie wykonana, zanim wyjątek zostanie przekazany do kodu wywołującego;

Wyjątek zawiera informacje o błędzie, który wystąpił. Można je uzyskać za pomocą następującej składni:


except MyException as exception:

[exception] to wyjątek, który wystąpił. [exception.args] reprezentuje krotkę parametrów wyjątku.

Aby wywołać wyjątek, stosuje się składnię


raise MyException(param1, param2…)

, gdzie najczęściej MyException jest klasą pochodną klasy BaseException. Parametry przekazane do konstruktora klasy będą dostępne w klauzuli „except” struktur przechwytywania wyjątków o składni [ex.args], jeśli [ex] jest wyjątkiem przechwyconym przez klauzulę [except].

Poniższy skrypt ilustruje te koncepcje.

5.2. skrypt [exceptions_02]

Poniższy skrypt w sposób jawny obsługuje błędy:


# obsługa wyjątków
essai = 0

# wywoływanie błędu i obsługa błędu
x = 2
try:
    x = 4 / 0
except ZeroDivisionError as erreur:
    # błąd jest przechwyconym wyjątkiem
    print(f"essai n° {essai} : {erreur}")
# wartość x nie uległa zmianie
print(f"x={x}")

# próbujemy ponownie
essai += 1
try:
    x = 4 / 0
except BaseException as erreur:
    # przechwytujemy najbardziej ogólny wyjątek
    # błąd jest przechwyconym wyjątkiem
    print(f"essai n° {essai} : {erreur}")

# można przechwycić różne typy wyjątków
# wykonanie zatrzymuje się na pierwszym [except] zdolnym do obsługi wyjątku
essai += 1
try:
    x = 4 / 0
except ValueError as erreur:
    # ten wyjątek nie występuje w tym miejscu
    print(f"essai n° {essai} : {erreur}")
except BaseException as erreur:
    # przechwytuje się najbardziej ogólny wyjątek
    print(f"essai n° {essai} : (Exception) {erreur}")
except ZeroDivisionError as erreur:
    # przechwytuje się konkretny typ
    print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")

# próbujemy ponownie, zmieniając kolejność 
essai += 1
try:
    x = 4 / 0
except ValueError as erreur:
    # ten wyjątek nie występuje w tym miejscu
    print(f"essai n° {essai} : {erreur}")
except ZeroDivisionError as erreur:
    # przechwytujemy konkretny typ
    print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")
except BaseException as erreur:
    # przechwytujemy najbardziej ogólny wyjątek
    print(f"essai n° {essai} : (Exception) {erreur}")

# klauzula „except” bez argumentów
essai += 1
try:
    x = 4 / 0
except:
    # nie interesuje nas charakter wyjątku
    print(f"essai n° {essai} : il y a eu un problème")

# inny typ wyjątku
essai += 1
try:
    # x nie może zostać przekonwertowane na liczbę całkowitą
    x = int("x")
except ValueError as erreur:
    # błąd jest przechwyconym wyjątkiem
    print(f"essai n° {essai} : {erreur}")

# wyjątek przenosi informacje w krotce dostępnej dla programu
essai += 1
try:
    x = int("x")
except ValueError as erreur:
    # błąd jest przechwyconym wyjątkiem
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")

# można wywoływać wyjątki
essai += 1
try:
    raise ValueError("param1", "param2", "param3")
except ValueError as erreur:
    # błąd to przechwycony wyjątek
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")


# można tworzyć własne wyjątki
# muszą one pochodzić od klasy [BaseException]
class MyError(BaseException):
    pass


# wywołuje się wyjątek MyError
essai += 1
try:
    raise MyError("info1", "info2", "info3")
except MyError as erreur:
    # błąd to przechwycony wyjątek
    print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")

# wyzwalany jest wyjątek MyError z komunikatem o błędzie
essai += 1
try:
    raise MyError("mon msg d'erreur")
except MyError as erreur:
    # błąd to przechwycony wyjątek
    print(f"essai n° {essai} : {erreur.args[0]}")

# klauzula finally jest zawsze wykonywana
# niezależnie od tego, czy wystąpi wyjątek, czy nie
essai += 1
x = None
try:
    x = 1
except:
    # wyjątek
    print(f"essai n° {essai} : exception")
finally:
    # wykonana w każdym przypadku
    print(f"essai n° {essai} : finally x={x}")

essai += 1
x = None
try:
    x = 2 / 0
except:
    # wyjątek
    print(f"essai n° {essai} : exception")
finally:
    # wykonana w każdym przypadku
    print(f"essai n° {essai} : finally x={x}")

# nie ma obowiązku umieszczania klauzuli [except]
essai += 1
try:
    # powoduje błąd
    x = 4 / 0
finally:
    # wykonuje się w każdym przypadku
    print(f"essai n° {essai} : finally x={x}")

Uwagi:

  • wiersze 4–12: obsługa dzielenia przez zero;
  • wiersz 8: przechwytuje się dokładnie ten wyjątek, który wystąpił, i wyświetla się go;
  • wiersz 12: z powodu wystąpienia wyjątku zmienna x nie otrzymała wartości z wiersza 7, a zatem jej wartość nie uległa zmianie;
  • wiersze 14–21: powtarzamy tę samą operację, ale przechwytując wyjątek wyższego poziomu typu BaseException. Ponieważ wyjątek ZeroDivisionError pochodzi od klasy BaseException, klauzula except go zatrzyma;
  • wiersze 23–36: umieszczamy kilka klauzul except, aby obsłużyć różne typy wyjątków. Wykonana zostanie tylko jedna klauzula except lub żadna, jeśli wyjątek nie spełnia warunków żadnej klauzuli except;
  • wiersze 38–50: powtarzamy tę samą procedurę, zmieniając kolejność klauzul [except], aby pokazać ich rolę;
  • wiersze 52–58: klauzula except może nie mieć żadnego argumentu. W takim przypadku zatrzymuje ona wszystkie wyjątki;
  • wiersze 60–67: wprowadzają wyjątek ValueError;
  • wiersze 69–75: pobierane są informacje zawarte w wyjątku;
  • wiersze 77–83: przedstawiają sposób wywołania (raise) wyjątku;
  • wiersze 86–106: ilustrują wykorzystanie własnej klasy wyjątku MyError. Klasa MyError jest jedynie pochodną klasy bazowej BaseException. Nie dodaje ona niczego do swojej klasy bazowej. Teraz jednak można ją wyraźnie wymienić w klauzulach except;
  • wiersze 108–130: ilustrują użycie klauzuli finally;
  • wiersze 132–139: te wiersze pokazują, że klauzula [except] nie jest obowiązkowa;

Wyniki wyświetlane na ekranie są następujące:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_02.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_02.py", line 136, in <module>
    x = 4 / 0
ZeroDivisionError: division by zero
essai n° 0 : division by zero
x=2
essai n° 1 : division by zero
essai n° 2 : (Exception) division by zero
essai n° 3 : (ZeroDivisionError) division by zero
essai n° 4 : il y a eu un problème
essai n° 5 : invalid literal for int() with base 10: 'x'
essai n° 6 : invalid literal for int() with base 10: 'x', paramètres=("invalid literal for int() with base 10: 'x'",)
essai n° 7 : ('param1', 'param2', 'param3'), paramètres=('param1', 'param2', 'param3')
essai n° 8 : ('info1', 'info2', 'info3'), paramètres=('info1', 'info2', 'info3')
essai n° 9 : mon msg d'erreur
essai n° 10 : finally x=1
essai n° 11 : exception
essai n° 11 : finally x=None
essai n° 12 : finally x=None

Process finished with exit code 1

5.3. skrypt [exceptions_03]

Ten nowy skrypt ilustruje przekazywanie wyjątków w łańcuchu funkcji wywołujących:


# wyjątek niestandardowy
class MyError(BaseException):
    pass


# trzy funkcje
def f1(x: int) -> int:
    # wyjątki nie są obsługiwane – są automatycznie przekazywane w górę
    return f2(x)


def f2(y: int) -> int:
    # wyjątki nie są obsługiwane – są automatycznie przekazywane w górę
    return f3(y)


def f3(z: int) -> int:
    if (z % 2) == 0:
        # jeśli z jest parzyste, zgłaszany jest wyjątek
        raise MyError("exception dans f3")
    else:
        return 2 * z


# ---------- main

# wyjątki są przekazywane w górę łańcucha wywoływanych metod
# aż do momentu, gdy jakaś metoda je przechwyci. W tym przypadku będzie to metoda main
try:
    print(f1(4))
except MyError as erreur:
    print(f"type : {type(erreur)}, arguments : {erreur.args}")


# trzy inne funkcje, które wzbogacają przekazywane przez nią wyjątki
def f4(x: int) -> int:
    try:
        return f5(x)
    except MyError as erreur:
        # wzbogacamy wyjątek, a następnie ponownie go rzucamy
        raise MyError("exception dans f4", erreur)


def f5(y: int) -> int:
    try:
        return f6(y)
    except MyError as erreur:
        # wzbogacamy wyjątek, a następnie ponownie go zgłaszamy
        raise MyError("exception dans f5", erreur)


def f6(z: int) -> int:
    if (z % 2) == 0:
        # rzucamy wyjątek, jeśli z jest parzyste
        raise MyError("exception dans f6")
    else:
        return 2 * z


# ---------- main
try:
    print(f4(4))
except MyError as erreur:
    # wyświetlenie wyjątku
    print(f"type : {type(erreur)}, arguments : {erreur.args}")
    # można prześledzić stos wyjątków
    err = erreur
    # wyświetla się komunikat o błędzie
    print(err.args[0])
    # czy wyjątek jest enkapsulowany?
    while len(err.args) == 2 and isinstance(err.args[1], BaseException):
        # zmiana wyjątku
        err = err.args[1]
        # pierwszym argumentem jest komunikat o błędzie
        print(err.args[0])

Uwagi:

  • w wierszach 25–32, w wywołaniu main --> f1 --> f2 --> f3 (wiersz 30), wyjątek MyError wygenerowany przez f3 zostanie przekazany aż do main. Zostanie ona następnie przetworzona przez klauzulę except w wierszu 31;
  • wiersze 61–75: w wywołaniu main --> f4 --> f5 --> f6 (wiersz 62) wyjątek MyError wygenerowany przez f6 zostanie przekazany wstecz aż do main. Zostanie ona następnie przetworzona przez klauzulę except w wierszu 63. Tym razem, podczas przechodzenia w górę łańcucha funkcji wywołujących, wyjątek MyError, który się pojawia, jest sam w sobie zawarty w innym wyjątku;
  • wiersze 66–75: pokazują, jak przechodzić w górę stosu wyjątków;
  • wiersz 71: funkcja [isinstance(instance, Classe)] zwraca wartość True, jeśli obiekt [instance] jest typu [Classe] lub pochodnego. W tym przypadku wykorzystaliśmy wyjątek najwyższego poziomu [BaseException], co gwarantuje przechwycenie wszystkich wyjątków;

Wyniki wyświetlane na ekranie są następujące:

1
2
3
4
5
6
7
8
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_03.py
type : <class '__main__.MyError'>, arguments : ('exception dans f3',)
type : <class '__main__.MyError'>, arguments : ('exception dans f4', MyError('exception dans f5', MyError('exception dans f6')))
exception dans f4
exception dans f5
exception dans f6

Process finished with exit code 0