Перейти к содержимому
Теория · Исключения · урок 1 из 11

Трассировка и виды ошибок

Чему вы научитесь

Чему вы научитесь:

  • читать трассировку снизу вверх и находить строку, где случилась ошибка;
  • перехватывать исключения через try/except и выбирать порядок веток;
  • выполнять код после успеха через else и в любом случае через finally;
  • возбуждать исключения через raise и объявлять свои классы исключений.
Столбик из трёх ящиков report и average, нижний треснул, и пунктир от трещины идёт обратно вверх мимо каждого

Представьте столбик из трёх ящиков, поставленных друг под другом. Верхний ящик — код программы вне функций. Он поручает работу ящику report, а тот — нижнему ящику average. Каждый ящик ждёт, пока нижний закончит и вернёт ответ.

Теперь нижний ящик трескается: average не может выполнить свою работу. Весть о поломке поднимается по пунктиру обратно вверх, мимо каждого ящика. Если её никто не остановит, остановится вся программа.

Так ошибка во время работы программы часто случается не в строке с вызовом, а глубоко внутри функций. Функция average делит сумму чисел на их количество, и для пустого списка это деление на ноль:

Python
def average(nums):
    return sum(nums) / len(nums)


def report(nums):
    print("average:", average(nums))


report([4, 6])  # average: 5.0
report([])
TEXT
Traceback (most recent call last):
  File "main.py", line 10, in <module>
    report([])
  File "main.py", line 6, in report
    print("average:", average(nums))
                      ^^^^^^^^^^^^^
  File "main.py", line 2, in average
    return sum(nums) / len(nums)
           ~~~~~~~~~~^~~~~~~~~~~
ZeroDivisionError: division by zero

Давайте разберём, что произошло:

  1. Вызов report([4, 6]) работает: average возвращает 5.0, и программа печатает average: 5.0.
  2. Вызов report([]) передаёт пустой список в report, а та — в average.
  3. В average выражение len(nums) равно 0, и деление на ноль ломает работу.
  4. Ни average, ни report ошибку не обрабатывают, поэтому программа останавливается и печатает отчёт.

Чтение снизу вверх

Когда случается ошибка, Python создаёт исключение — объект, который описывает, что пошло не так. Если программа его не перехватывает, она останавливается и печатает трассировку. Трассировка — это отчёт о пути исключения: какие вызовы оно прошло и где возникло.

Трассировка перечисляет вызовы, через которые прошла программа. Что в ней есть:

  • на каждый вызов — строка с файлом, номером строки и именем функции;
  • под ней — сама строка кода;
  • имя <module> обозначает код вне функций, наш верхний ящик;
  • значки ^ и ~ отмечают часть строки, которая выполнялась в момент ошибки;
  • слова most recent call last значат, что последний вызов стоит внизу.

Читайте трассировку снизу вверх, начиная с последней строки:

  1. Последняя строка называет вид исключения и причину: ZeroDivisionError, деление на ноль.
  2. Над ней — место ошибки: строка 2 в функции average, треснувший ящик.
  3. Ещё выше — кто её вызвал: строка 6 в report.
  4. В самом верху — строка 10 в <module>, откуда всё началось.

Исправлять часто нужно не последнюю функцию, а вызов, который передал ей неподходящие данные, — здесь пустой список.

Здесь аналогия с ящиками немного расходится с экраном. Исключение поднимается снизу вверх, а трассировка печатает ящики сверху вниз, в порядке вызовов. Поэтому глаза идут от последней строки к первой.

Виды исключений

Вид исключения подсказывает, что искать, а текст после двоеточия уточняет причину. У KeyError это сам ключ, которого нет, у ValueError — значение, которое не подошло. Эти пять видов встречаются чаще других:

ВидПримерКогда возникает
ValueErrorint("ten")тип подходит, а значение — нет
TypeError"a" + 1операция не подходит к типу
KeyError{"a": 1}["b"]в словаре нет такого ключа
IndexError[1, 2][5]индекс за концом списка или строки
ZeroDivisionError1 / 0деление на ноль, в том числе // и %

Семейство исключений

Каждый вид исключения — это класс, а само исключение — экземпляр этого класса. Классы исключений связаны наследованием: ValueError — дочерний класс Exception, а Exception — дочерний класс BaseException. Потомки класса — его дочерние классы, их дочерние классы и так далее.

Все пять классов из таблицы — потомки Exception, а все исключения — потомки BaseException. Экземпляр потомка считается и экземпляром каждого класса выше него. Экземпляр исключения можно создать и без ошибки, чтобы проверить это через isinstance:

Python
error = ValueError("bad")
print(isinstance(error, ValueError))  # True
print(isinstance(error, Exception))  # True
print(isinstance(error, BaseException))  # True
print(isinstance(KeyboardInterrupt(), Exception))  # False

Исключение KeyboardInterrupt возникает, когда программу останавливают сочетанием клавиш Ctrl+C. Оно наследует BaseException напрямую, минуя Exception: это не ошибка в программе, а команда остановиться. Все ошибки самой программы — потомки Exception.

На собеседовании часто спрашивают, чем Exception отличается от BaseException. Ответ:

  • BaseException — корень всех исключений;
  • Exception — его потомок и родитель всех ошибок программы;
  • команды остановки вроде KeyboardInterrupt наследуют BaseException напрямую, минуя Exception.

Главное

  • Исключение — объект, который описывает ошибку; непойманное исключение останавливает программу и печатает трассировку.
  • Трассировку читают снизу вверх: последняя строка — вид и причина, над ней — место ошибки, выше — кто вызывал.
  • Каждый вид исключения — класс; все ошибки программы — потомки Exception, а все исключения — потомки BaseException.
  • KeyboardInterrupt наследует BaseException напрямую, поэтому он не потомок Exception.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом