5. Винятки
Тепер розглянемо винятки.

5.1. скрипт [exceptions_01]
Перший скрипт ілюструє необхідність обробки винятків.
Ми навмисно викликаємо помилку, щоб побачити, що відбудеться (exceptions-01.py):
У рядку 4 ми бачимо:
- тип винятку: ZeroDivisionError;
- відповідне повідомлення про помилку: division by zero. Воно англійською мовою. Це те, що, можливо, варто змінити.
Основне правило полягає в тому, що слід робити все можливе, щоб уникнути винятків, які генерує інтерпретатор Python. Ми повинні обробляти помилки самостійно.
Синтаксис обробки винятків такий:
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
У блоці try виконання дій зупиняється, щойно виникає виняток (помилка). У цьому випадку виконання продовжується з дій одного з блоків except:
- рядок 3: якщо виняток [ex], що виник, належить до типу, який входить до кортежу (Ex1, Ex2…) або є похідним від одного з них, то виконуються дії з рядка 4;
- рядок 5: якщо виняток не був перехоплений рядком 3, і існує інший блок [except], то відбувається той самий процес. І т. д.;
- може бути стільки умов [except], скільки потрібно для обробки різних типів винятків, що можуть виникнути в [try];
- якщо виняток не було оброблено жодним із блоків [except], то він передається до коду, що його викликав. Якщо цей код сам знаходиться у структурі try / except, виняток знову обробляється; інакше він продовжує передаватися вгору по ланцюжку викликаних методів. В кінцевому підсумку вона потрапляє до інтерпретатора Python. Той зупиняє виконувану програму та виводить повідомлення про помилку, подібне до наведеного у попередньому прикладі. Отже, правило полягає в тому, що головна програма повинна перехоплювати всі винятки, які можуть передаватися з викликаних методів;
- рядок 7: оператор [finally] завжди виконується незалежно від того, чи сталося виключення (продовження блоку
except**), чи ні (продовження блокуtry**). Це справедливо навіть у тому випадку, якщо виключення сталося, але не було перехоплене. У цьому випадку оператор [finally] буде виконано до того, як виняток буде передано до коду, що викликає;
Виняток містить інформацію про помилку, що сталася. Її можна отримати за допомогою такого синтаксису:
except MyException as exception:
[exception] — це виняток, що стався. [exception.args] представляє кортеж параметрів винятку.
Щоб викликати виняток, використовується синтаксис
raise MyException(param1, param2…)
, де найчастіше MyException є класом, похідним від класу BaseException. Параметри, передані конструктору класу, будуть доступні в блоці except структур перехоплення винятків із синтаксисом [ex.args], якщо [ex] є винятком, перехопленим блоком [except].
Ці поняття проілюстровано в наведеному нижче скрипті.
5.2. скрипт [exceptions_02]
Наступний скрипт явно обробляє помилки:
# обробка винятків
essai = 0
# викликаємо помилку та обробляємо її
x = 2
try:
x = 4 / 0
except ZeroDivisionError as erreur:
# помилка є перехопленим винятком
print(f"essai n° {essai} : {erreur}")
# значення x не змінилося
print(f"x={x}")
# повторюємо
essai += 1
try:
x = 4 / 0
except BaseException as erreur:
# перехоплюємо найзагальніше виключення
# помилка є перехопленим винятком
print(f"essai n° {essai} : {erreur}")
# можна перехоплювати різні типи винятків
# виконання зупиняється на першому [except], здатному обробити виняток
essai += 1
try:
x = 4 / 0
except ValueError as erreur:
# це виключення тут не виникає
print(f"essai n° {essai} : {erreur}")
except BaseException as erreur:
# перехоплюється найзагальніше виключення
print(f"essai n° {essai} : (Exception) {erreur}")
except ZeroDivisionError as erreur:
# перехоплюється конкретний тип
print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")
# починаємо спочатку, змінивши порядок
essai += 1
try:
x = 4 / 0
except ValueError as erreur:
# це виключення тут не виникає
print(f"essai n° {essai} : {erreur}")
except ZeroDivisionError as erreur:
# перехоплюється конкретний тип
print(f"essai n° {essai} : (ZeroDivisionError) {erreur}")
except BaseException as erreur:
# перехоплюється найзагальніше виключення
print(f"essai n° {essai} : (Exception) {erreur}")
# виключення без аргументів
essai += 1
try:
x = 4 / 0
except:
# характер винятку не має значення
print(f"essai n° {essai} : il y a eu un problème")
# інший тип винятку
essai += 1
try:
# x не можна перетворити на ціле число
x = int("x")
except ValueError as erreur:
# помилка є перехопленим винятком
print(f"essai n° {essai} : {erreur}")
# виняток передає інформацію у кортежі, доступному для програми
essai += 1
try:
x = int("x")
except ValueError as erreur:
# помилка є перехопленим винятком
print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
# винятки можна генерувати
essai += 1
try:
raise ValueError("param1", "param2", "param3")
except ValueError as erreur:
# помилка — це перехоплене виключення
print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
# можна створювати власні винятки
# вони повинні походити від класу [BaseException]
class MyError(BaseException):
pass
# викликається виняток MyError
essai += 1
try:
raise MyError("info1", "info2", "info3")
except MyError as erreur:
# помилка є перехопленим винятком
print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
# викликається виняток MyError із повідомленням про помилку
essai += 1
try:
raise MyError("mon msg d'erreur")
except MyError as erreur:
# помилка — це перехоплене виключення
print(f"essai n° {essai} : {erreur.args[0]}")
# блока finally завжди виконується
# незалежно від того, чи виникло виключення
essai += 1
x = None
try:
x = 1
except:
# виняток
print(f"essai n° {essai} : exception")
finally:
# виконується в будь-якому випадку
print(f"essai n° {essai} : finally x={x}")
essai += 1
x = None
try:
x = 2 / 0
except:
# виняток
print(f"essai n° {essai} : exception")
finally:
# виконується в будь-якому випадку
print(f"essai n° {essai} : finally x={x}")
# не обов'язково вказувати клаузулу [except]
essai += 1
try:
# викликає помилку
x = 4 / 0
finally:
# виконується в будь-якому випадку
print(f"essai n° {essai} : finally x={x}")
Примітки:
- рядки 4–12: обробляється поділ на нуль;
- рядок 8: перехоплюється саме те виключення, яке виникло, і виводиться на екран;
- рядок 12: через виникнення винятку x не отримав значення з рядка 7 і, отже, не змінив свого значення;
- рядки 14–21: повторюємо те саме, але перехоплюємо виняток вищого рівня типу BaseException. Оскільки виняток ZeroDivisionError походить від класу BaseException, оператор except зупинить його;
- рядки 23–36: додаємо кілька умов except для обробки різних типів винятків. Буде виконана лише одна умова except або жодна, якщо виняток не відповідає жодній умові except;
- рядки 38–50: повторюємо те саме, змінюючи порядок клаузул [except], щоб продемонструвати їхню роль;
- рядки 52–58: клаузула except може не мати жодного аргументу. У цьому випадку вона зупиняє всі винятки;
- рядки 60–67: вводиться виняток ValueError;
- рядки 69–75: отримується інформація, що передається винятком;
- рядки 77–83: описують спосіб виклику (raise) винятку;
- рядки 86–106: ілюструють використання власного класу винятків MyError. Клас MyError просто походить від базового класу BaseException. Він нічого не додає до свого базового класу. Але тепер його можна явно вказати в операторах except;
- рядки 108–130: ілюструють використання оператора finally;
- рядки 132–139: ці рядки показують, що клаузула [except] не є обов’язковою;
Результати на екрані такі:
5.3. скрипт [exceptions_03]
Цей новий скрипт ілюструє передачу винятків у ланцюжку викликів функцій:
# власне виняток
class MyError(BaseException):
pass
# три функції
def f1(x: int) -> int:
# винятки не обробляються — вони автоматично передаються вище
return f2(x)
def f2(y: int) -> int:
# винятки не обробляються — вони автоматично передаються вище
return f3(y)
def f3(z: int) -> int:
if (z % 2) == 0:
# якщо z парне, генерується виняток
raise MyError("exception dans f3")
else:
return 2 * z
# ---------- main
# винятки передаються вгору по ланцюжку викликаних методів
# доти, доки якийсь метод не перехопить його. У цьому випадку це буде main
try:
print(f1(4))
except MyError as erreur:
print(f"type : {type(erreur)}, arguments : {erreur.args}")
# ще три функції, які доповнюють винятки, які вона передає далі
def f4(x: int) -> int:
try:
return f5(x)
except MyError as erreur:
# виняток доповнюється, а потім перекидається
raise MyError("exception dans f4", erreur)
def f5(y: int) -> int:
try:
return f6(y)
except MyError as erreur:
# збагачуємо виняток, а потім перекидаємо його
raise MyError("exception dans f5", erreur)
def f6(z: int) -> int:
if (z % 2) == 0:
# генерується виняток, якщо z парне
raise MyError("exception dans f6")
else:
return 2 * z
# ---------- main
try:
print(f4(4))
except MyError as erreur:
# виведення інформації про виняток
print(f"type : {type(erreur)}, arguments : {erreur.args}")
# можна простежити стек винятків
err = erreur
# виводиться повідомлення про помилку
print(err.args[0])
# чи є виняток інкапсульованим?
while len(err.args) == 2 and isinstance(err.args[1], BaseException):
# зміна винятку
err = err.args[1]
# перший аргумент — це повідомлення про помилку
print(err.args[0])
Примітки:
- у рядках 25–32, під час виклику main --> f1 --> f2 --> f3 (рядок 30), виняток MyError, згенерований функцією f3, буде передаватися вгору до функції main. Потім вона буде оброблена за допомогою оператора except у рядку 31;
- рядки 61–75: під час виклику main → f4 → f5 → f6 (рядок 62) виняток MyError, згенерований функцією f6, буде передаватися вгору до функції main. Потім вона буде оброблена за допомогою оператора except у рядку 63. Цього разу, під час проходження ланцюжка викликів функцій, виняток MyError, що просувається вгору, сам інкапсульований в інший виняток;
- рядки 66–75: показують, як просуватися вгору по стеку винятків;
- рядок 71: функція [isinstance(instance, Classe)] повертає True, якщо об’єкт [instance] є типом [Classe] або його похідним. Тут ми використали виняток найвищого рівня [BaseException], що гарантує нам отримання всіх винятків;
Результати виведення на екран такі: