Программирование и IT

Python: исключения

Исключение — сообщение о том, что выполнение пошло не так, как предполагал код. По умолчанию оно останавливает программу и печатает трассировку. Конструкция `try/except` позволяет перехватить только ожидаемую ошибку и продолжить работу, не пряча при этом настоящие поломки.

Обновлено
В этой статье

Как выглядит ошибка и что в ней читать#

print(int("сто"))
  # Traceback (most recent call last):
  #   File "<stdin>", line 1, in <module>
  # ValueError: invalid literal for int() with base 10: 'сто'

В трассировке важны две вещи: последняя строка — тип и текст ошибки, а строки
выше — путь, по которому до неё добрались. Читать её нужно снизу вверх: сначала
что случилось, потом где.

Типы исключений образуют дерево. Почти всё, что ловят в прикладном коде,
наследуется от Exception:

  • ValueError — тип подходит, значение нет (int("сто"));
  • TypeError — тип не подходит ("a" + 1);
  • KeyError и IndexError — нет такого ключа или индекса;
  • ZeroDivisionError — деление на ноль;
  • FileNotFoundError — нет файла;
  • AttributeError — у объекта нет такого атрибута.

try/except: минимум#

try:
    age = int(input("возраст: "))
except ValueError:
    print("нужно целое число")
    age = 0
print(age)

Блок try содержит только рискованную строку — чем он короче, тем точнее видно,
что именно защищено. Типов можно указать несколько, а сам объект ошибки
доступен через as:

data = {"a": 1}
try:
    print(data["b"] / 0)
except (KeyError, ZeroDivisionError) as error:
    print(type(error).__name__, "", error)
  # KeyError — 'b'

Несколько блоков except проверяются сверху вниз, и срабатывает первый
подходящий. Поэтому частные типы пишут выше общих: except Exception первым
перехватит всё и до остальных дело не дойдёт.

else и finally#

try:
    value = int("42")
except ValueError:
    print("не число")
else:
    print("разобрали:", value)     # только если исключения не было
finally:
    print("этот блок выполнится всегда")
  # разобрали: 42
  # этот блок выполнится всегда

else выполняется, когда try прошёл без ошибок, — туда выносят код, который
не должен быть защищён этим except. finally выполняется в любом случае: и
при ошибке, и при return, и при выходе по исключению дальше. В нём закрывают
ресурсы — хотя для файлов и соединений удобнее оператор with, описанный в
статье про чтение файла.

raise и свои классы ошибок#

Бросить исключение самому — одна строка. Это правильный способ сообщить о
недопустимых аргументах: функция не должна молча возвращать None.

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError(f"сумма должна быть положительной, получено {amount}")
    if amount > balance:
        raise ValueError("недостаточно средств")
    return balance - amount

try:
    withdraw(100, -5)
except ValueError as error:
    print("отказ:", error)
  # отказ: сумма должна быть положительной, получено -5

Когда стандартных типов не хватает, заводят свой — обычно пустым классом:

class ConfigError(Exception):
    """Настройки заданы неверно."""

def load(cfg):
    if "host" not in cfg:
        raise ConfigError("в настройках нет ключа host")

try:
    load({})
except ConfigError as error:
    print(error)      # в настройках нет ключа host

Перехватить и перебросить с сохранением причины помогает raise ... from:
raise ConfigError("не прочитали") from error — в трассировке останутся оба
исключения, и видно, что было первопричиной.

Чего не делать#

except: без типа. Такой блок ловит вообще всё, включая KeyboardInterrupt
и SystemExit, — программу становится невозможно прервать с клавиатуры. Если
нужна широкая сеть, пиши except Exception, и лучше с указанием, что
случилось.

except ...: pass. Молча проглоченная ошибка возвращается через час в виде
пустого отчёта, и найти её причину уже нечем. Минимум — напечатать или записать
в журнал.

Огромный try. Когда под try двадцать строк, except ValueError
относится ко всем двадцати, и ты перехватишь ошибку совсем не там, где ждал.

Исключения вместо условий. Проверка if x > 0 не нуждается в try.
Но и обратная крайность вредна: в Python принято пробовать и ловить, а не
заранее опрашивать состояние — d.get(key) и try/except KeyError лучше, чем
if key in d перед каждым обращением, когда обращений много.

Практика: напиши функцию safe_div(a, b), которая возвращает частное, а при
делении на ноль — None с сообщением; затем функцию parse_ages(строки),
которая превращает список строк в числа, пропуская нечисловые и сообщая о
каждом пропуске. Пригодятся списки и
функции.

План по этапам

  1. Прочитать трассировкуВыполнить int("сто") и разобрать вывод: тип, текст, место.
  2. Первый try/exceptЗащитить преобразование строки в число и выдать понятное сообщение вместо падения.
  3. Несколько типовПерехватить KeyError и ZeroDivisionError одним блоком, напечатать тип ошибки через type(error).__name__.
  4. Добавить finallyУбедиться, что блок finally выполняется и при ошибке, и без неё.
  5. Свой класс ошибкиЗавести наследника Exception, бросить его из функции и поймать в вызывающем коде.

Начать изучать эту тему у себя

План ляжет в твой репозиторий: отмечай этапы, веди конспект — история изменений покажет, как ты продвинулся.

Начать план

Проверь себя

1.Какой тип исключения даст int("сто")?

2.Что выведет: try: x = 1 / 0 / except ZeroDivisionError: x = 7 / finally: x += 1 — чему равен x?

3.Какой блок выполняется только тогда, когда в try не было исключения?

4.Чем плох except: без указания типа?

Источники

Было полезно?

Ещё темы

Программирование и IT Как изучить Python с нуля Python хорош для первого знакомства с программированием: код читается почти как текст, а стандартная библиотека закрывает большинство бытовых задач. Этот план ведёт от установки интерпретатора до собственных скриптов, покрытых тестами, — примерно за четыре месяца при занятиях около часа в день. Программирование и IT Python: задачи с решениями Подборка задач по возрастанию сложности — от разворота строки до генератора и декоратора-счётчика. Сначала решай сам и только потом открывай разбор: сравнение своего кода с чужим даёт больше, чем чтение готового. Все решения проверены на Python 3. Программирование и IT Python: генераторы Генератор выдаёт значения по одному и только тогда, когда их просят. Он не хранит всю последовательность в памяти, поэтому подходит для больших файлов и бесконечных потоков. Плата за это — генератор одноразовый: пройти по нему дважды не получится. Программирование и IT Python: срезы Срез вырезает из последовательности кусок по правилу «с какого индекса, до какого, с каким шагом». Запись компактная, но у неё два места, где почти все ошибаются: правая граница не включается, а при отрицательном шаге границы меняются местами. Разберём оба. Программирование и IT Python: декораторы Декоратор — это функция, которая принимает другую функцию и возвращает новую, с добавленным поведением. Знак `@` перед определением — просто короткая запись присваивания. Если держать это в голове, вся тема разбирается за один вечер. Программирование и IT Python: функции Функция — именованный кусок кода, который принимает значения и возвращает результат. В Python объявление занимает одну строку, но у аргументов есть тонкости: умолчания вычисляются один раз, именованные и позиционные смешиваются по правилам, а без return функция всё равно что-то возвращает.

Ещё сценарии