Помилки та винятки

Існує (принаймні) два типи помилок: синтаксичні помилки та виключення [docb].

Синтаксичні помилки

Синтаксичні помилки також відомі як помилки Python парсера.

>>> while True print("the loop is running...")
  File "<string>", line 1
    while True print('the loop is running...')
               ^^^^^
SyntaxError: invalid syntax

Синтаксичний аналізатор повторює неправильний рядок і відображає маленьку «стрілку», яка вказує на найперший символ рядка, де було виявлено помилку. Помилка спричинена (або принаймні виявлена) символом(а) перед стрілкою.

Винятки

Навіть якщо оператор або вираз синтаксично правильний, це може спричинити помилку під час спроби його виконання. Помилки, виявлені під час виконання, називаються винятками і не є фатальними: незабаром ви дізнаєтеся, як їх обробляти в програмах на Python.

Винятки бувають різних типів, і тип друкується як частина повідомлення.

Наприклад, функція div, реалізована як

def div(x: Union[int, float], y: Union[int, float]) -> float:
    """Return the result of dividing `x` by `y`."""

    return x / y

може викликати виняток ZeroDivisionError, якщо 0 передається як параметр y.

Стандартні назви винятків є вбудованими ідентифікаторами (а не зарезервованими ключовими словами).

Дивись також

Список вбудованих винятків

Відстеження

Примітка

У наведених прикладах «Файл» замінено на фіктивний рядок.

Припустимо наступну структуру модулів:

/
|-- main.py
|-- func.py

Модуль main імпортує функцію div, реалізовану вище, з модуля func. Основний код реалізовано наступним чином:

from func import div

if __name__ == "__main__":
    x = int(input("Please enter a number: "))
    y = int(input("Please enter a number: "))
    print(f"{x = }, {y = } -> {div(x, y) = }")

Під час виконання коду з правильними введеннями він працюватиме нормально. Але для цього сценарію є два винятки.

Перший пов’язаний з перетворенням типів. Під час виконання основного сценарію є випадок неправильного введення.

Please enter a number: 100
Please enter a number: one
Traceback (most recent call last):
  File "path/to/module.py", line 5, in <module>
    y = int(input("Please enter a number: "))
ValueError: invalid literal for int() with base 10: 'one'

Зауважте, що traceback вказує на модуль і точний рядок коду, який викликає виняток.

Другий виняток виникає, коли 0 передається як другий параметр функції div. Трасування виглядатиме так:

Please enter a number: 100
Please enter a number: 0
Traceback (most recent call last):
  File "path/to/main.py", line 6, in <module>
    print(f"{x = }, {y = } -> {div(x, y) = }")
  File "path/to/func.py", line 11, in div
    return x / y
ZeroDivisionError: division by zero

Зворотне відстеження вказує на всі виклики (зверху вниз), які викликають виняток.

Обробка винятків

Можна писати програми, які оброблятимуть вибрані винятки. Для цього використовується інструкція try.

Інструкція try працює наступним чином.

  1. Спочатку виконується речення try (вираз(и) між ключовими словами try і except).

  2. Якщо винятків не відбувається, пропозиція винятку пропускається, і виконання оператора try завершується.

  3. Якщо під час виконання try виникає виняток, решта пропозиції пропускається. Потім, якщо його тип збігається з винятком, названим за ключовим словом osim, виконується пропозиція, а потім виконання продовжується після блоку try/except.

  4. Якщо виникає виняток, який не збігається з винятком, названим у реченні винятку, він передається зовнішнім операторам try; якщо обробник не знайдено, це необроблена виняткова ситуація, і виконання зупиняється з повідомленням, як показано вище.

from func import div

if __name__ == "__main__":
    x = int(input("Please enter a number: "))
    y = int(input("Please enter a number: "))
    try:
        print(f"{x = }, {y = } -> {div(x, y) = }")
    except:
        print("An exception occurs")

Інструкція try може мати більше однієї пропозиції, щоб вказати обробники для різних винятків.

from func import div

if __name__ == "__main__":
    x = int(input("Please enter a number: "))
    y = int(input("Please enter a number: "))
    try:
        print(f"{x = }, {y = } -> {div(x, y) = }")
    except (ValueError, ZeroDivisionError):
        print("An exception occurs")
from func import div

if __name__ == "__main__":
    x = int(input("Please enter a number: "))
    y = int(input("Please enter a number: "))
    try:
        print(f"{x = }, {y = } -> {div(x, y) = }")
    except ValueError as exc:
        print(f"Value cannot be converted, {exc}")
    except ZeroDivisionError:
        print(f"Zero division error: {y = }")

Опрацювання усіх винятків

Обробляти всі винятки – це погана практика.

Якщо вказати блок except без визначення винятку для перехоплення, він працюватиме для будь-якого винятку, який виникає в блоці try. Те саме стосується обробки BaseException і Exception. Проте ніколи не можливо сказати, який саме виняток стався.

Примітка

Проте, такий сценарій може працювати для логування винятків, після чого вони підійматься заново, щоб бути опрацьованими на вищих рівнях програми.

try:
    ...    # some code that may raise an exception
except:
    ...    # logging actions
    raise  # raise an exception again
except:
    ...

except (BaseException, Exception):
    ...

except Exception:
    ...

Усі винятки успадковуються від класу Exception, який є підкласом BaseException. Отже, ви ніколи не можете знати, що саме пішло не так із вашим кодом.

Вираз finally

Оператор try не може бути використаний сам по собі. Це спричинить SyntaxError. Необхідно включити блок except або finally.

У той час як блок except намагається перехопити винятки, якщо такі є, блок finally завжди виконуватиметься незалежно від виникнення винятку.

try:
    ...  # some code that may cause an exception
finally:
    ...  # this code will be always executed

try:
    result = 10 // 2
except ZeroDivisionError:
    print("zero division")
finally:
    print("it's finished")

try:
    result = 10 // 0
except ZeroDivisionError:
    print("zero division")
finally:
    print("it's finished")

Вираз else

Оператор try також може використовувати else (який є необов’язковим). Інструкції в цьому блоці виконуватимуться лише у випадку, якщо під час виконання try не було викликано винятків.

try:
    result = 10 // 2
except ZeroDivisionError:
    print("zero division")
else:
    print("no errors occurred")  # this **WILL** be printed out

try:
    result = 10 // 0
except ZeroDivisionError:
    print("zero division")
else:
    print("no errors occurred")  # this **WILL NOT** be printed out

Винятки: raising

Інструкція raise дозволяє програмісту примусово викликати певний виняток. Єдиний аргумент, який потрібно викликати, вказує на виняток, який потрібно викликати. Це має бути екземпляр винятку або клас винятку (клас, який походить від BaseException, наприклад Exception або один із його підкласів). Якщо передано клас винятків, він буде неявно створений шляхом виклику його конструктора без аргументів.

raise NameError("name error")
raise NameError

Сам оператор raise повторно викличе всі винятки в контексті опрацювання. Крім того, винятки можуть бути викликані знову з блоку except. Це загальний шаблон для перехоплення винятку для реєстрації повідомлення про виняткову ситуацію і повторного його підняття.

try:
    result = 10 // 0
except:
    print("zero division")
    raise

Створення винятків

Програми можуть називати власні винятки, створюючи новий клас винятків. Винятки зазвичай мають бути похідними від класу Exception, прямо чи опосередковано.

Можна визначити класи винятків, які роблять усе, що може зробити будь-який інший клас, але зазвичай вони прості, часто пропонують лише ряд атрибутів, які дозволяють обробникам витягувати інформацію про помилку для винятку.

Більшість винятків визначено з іменами, які закінчуються на «Error», подібно до назв стандартних винятків.

Багато стандартних модулів визначають власні винятки, щоб повідомляти про помилки, які можуть виникнути у функціях, які вони визначають.

Виняток, визначений користувачем
class InvalidInputError(Exception):
    """This is a custom exception for demo needs"""


class NegativeError(ValueError):
    """Raised when a negative value passed"""


def sum_positive(x: int, y: int) -> int:
    """Return the sum of positive integers"""

    if x < 0 or y < 0:
        raise NegativeError

    return x + y


try:
    x = int(input("Enter number x: "))
    y = int(input("Enter number y: "))
    result = sum_positive(x, y)
except ValueError:
    raise InvalidInputError("could not convert to integer")
except NegativeError:
    print("this function works only with positive integers")