Как обрабатывать исключения в Python: try, except, else и finally на практических примерах

Обработка исключений нужна не для того, чтобы программа никогда не показывала ошибку. Её задача — различить ожидаемую ситуацию, которую код умеет исправить, и сбой, с которым нельзя честно продолжать работу. Если вместо неверного числа незаметно подставить ноль, программа может закончиться без аварии и выдать неправильный результат.
Рассмотрим авторский учебный пример: маленькая программа читает количество товаров из текста и считает стоимость заказа. Постепенно добавим ввод, чтение файла и пояснение причины ошибки. Суммы и имена файлов условные; код использует обычные конструкции Python 3.
Сначала прочитайте тип и место ошибки
Не всякая ошибка означает одно и то же. int('три') вызывает ValueError: передан текст, но он не представляет целое число. Обращение к отсутствующему ключу словаря может вызвать KeyError, деление на ноль — ZeroDivisionError. Тип помогает выбрать реакцию, а traceback показывает путь выполнения до места сбоя.
Синтаксическую ошибку в самом файле не исправляют оборачиванием следующей строки в try: сначала Python должен разобрать программу. При выполнении отдельных операций разбора тоже могут возникать исключения, но для первого знакомства важно не смешивать ошибку написания конструкции и ошибку данных во время её работы.
Уберите из первоначального примера обработчики и посмотрите, что сообщает интерпретатор. Затем добавляйте except именно для того случая, который вы понимаете. Такой порядок полезнее, чем сразу окружить всю программу большим try и потерять место возникновения проблемы.
Минимальный try/except
raw = 'три'
try:
quantity = int(raw)
except ValueError:
print('Введите целое число цифрами')
else:
print('Количество:', quantity)
В try помещена операция преобразования. Если она завершается с ValueError, выполняется соответствующий обработчик. Если исключение другого типа не подходит ни одному обработчику, оно передаётся наружу. Код не продолжает со следующей строкой внутри прерванного try: после обработки управление переходит за всю конструкцию.
Почему мы не пишем quantity=0 в except? Для учебного заказа это означало бы выдать ошибку пользователя за реальное количество товаров. Выбранная реакция — объяснить проблему; следующий шаг зависит от интерфейса: повторный запрос, сообщение формы или завершение команды.
Разделяйте неверный формат и недопустимое значение
Число может успешно преобразоваться и всё же не подходить задаче. В нашем примере количество должно быть положительным. Опишем собственную проверку через raise:
def parse_quantity(text):
quantity = int(text)
if quantity <= 0:
raise ValueError('Количество должно быть положительным')
return quantity
for raw in ['4', '0', 'три']:
try:
quantity = parse_quantity(raw)
except ValueError as exc:
print(raw, '— ошибка:', exc)
else:
print(raw, '— принято:', quantity)
Для '4' получим принятое количество 4; '0' отклонит наше бизнес-условие, а 'три' — преобразование int(). Оба случая представлены ValueError, но причины и сообщения разные. Если приложению нужно реагировать на них совершенно по-разному, можно проектировать собственные классы исключений. Для этого небольшого опыта достаточно сохранить конкретное объяснение.
Проверка выше ожидает строку. Если случайно передать список, проблема уже в вызывающем коде. Не стоит без необходимости ловить все возможные типы ошибок только для того, чтобы вывести одинаковую подпись «неверное количество».
except: выбирайте узкий участок и конкретные классы
При чтении файла могут возникнуть разные ситуации: файл отсутствует, нет доступа, содержимое не подходит ожидаемой кодировке. Они не равны неверной записи целого числа. Покажем загрузку и преобразование раздельно:
def read_text(path):
with open(path, encoding='utf-8') as stream:
return stream.read().strip()
try:
raw = read_text('quantity.txt')
except FileNotFoundError:
print('Создайте файл quantity.txt')
except PermissionError:
print('Проверьте права доступа к файлу')
except UnicodeDecodeError:
print('Ожидается текст в UTF-8')
else:
try:
quantity = parse_quantity(raw)
except ValueError as exc:
print('Неверное содержимое файла:', exc)
else:
print('Стоимость:', quantity * 250)
Если в quantity.txt записано 4, учебная стоимость равна 1000. Если файл пуст, int() не получает запись числа, и это ошибка содержимого, а не отсутствие файла. При слишком широком except эти ситуации слились бы в одно сообщение и затруднили исправление.
Обработчики проверяются по порядку, а дочерние классы подходят обработчику родительского класса. Поэтому более конкретный FileNotFoundError размещают раньше общего OSError, если оба используются в одной конструкции. Exception охватывает множество обычных ошибок, но не является синонимом вообще всех возможных сигналов завершения, включая KeyboardInterrupt.
Зачем нужен else
Блок else выполняется, когда защищённая часть завершилась без исключения. Он помогает не помещать в try слишком много кода. В первом примере преобразование защищено от ValueError, а печать принятого результата находится отдельно. Если ошибка произойдёт уже в else, её не поймает except этой же конструкции.
Это полезная граница ответственности. Например, после успешного чтения файла может следовать отдельное преобразование или запись результата. Отказ второго действия не должен автоматически превращаться в сообщение «не удалось прочитать файл». При росте программы разносите такие этапы по небольшим функциям, а обработчик ставьте там, где действительно понятно, как ответить на сбой.
finally: завершение действия, а не подавление причины
При обычном завершении конструкции finally выполняется и после успешной работы, и при исключении, в том числе перед его передачей наружу. На небольшом примере это видно без работы с файлами:
def inspect_quantity(raw):
try:
quantity = parse_quantity(raw)
except ValueError as exc:
print('Отклонено:', exc)
else:
print('Принято:', quantity)
finally:
print('Проверка этого значения завершена')
inspect_quantity('2')
inspect_quantity('нет')
Сначала вы увидите принятое значение, затем отклонённое; завершающее сообщение появится в обоих случаях. Но finally сам по себе не превращает ошибочный результат в правильный и не означает успешное выполнение операции.
Для закрытия обычного файла предпочитайте with: он делает границу использования ресурса явной. Не добавляйте return в finally ради удобства — такой выход способен подавить исключение и скрыть настоящий сбой. Если от finally ожидается только освобождение ресурса или завершение наблюдения, не смешивайте это с выбором результата функции.
Повторный выброс и сохранение причины
Иногда нужно добавить контекст, но не скрыть первопричину. Bare raise внутри обработчика повторно выбрасывает текущее исключение. А конструкция raise ... from ... создаёт новое объяснение с явной цепочкой причин:
def load_quantity(path):
raw = read_text(path)
try:
return parse_quantity(raw)
except ValueError as exc:
raise ValueError(
f'В файле {path} неверно задано количество'
) from exc
Здесь ошибка открытия остаётся ошибкой открытия: мы не перехватываем её как ValueError. Если же текст не подходит, вызывающий код получает контекст файла, а исходная причина остаётся доступной в цепочке исключений. На верхнем уровне приложение решит, показывать ли пользователю короткое сообщение, а разработчику — диагностическую информацию.
Что проверить перед использованием обработчика
Пустой except с pass часто оставляет программу в неопределённом состоянии: важное действие не состоялось, а внешне всё выглядит завершённым. Для нашего примера попробуйте четыре случая: '4', '0', 'три' и отсутствующий файл. Сравните сообщения и убедитесь, что цена не рассчитывается после ошибки ввода.
Ожидаемый сбой должен иметь понятный исход: повтор ввода, отказ от операции или передача ошибки дальше. Если вы не знаете, что делать после исключения, сохранение причины и остановка соответствующего действия честнее, чем продолжение с придуманным значением. Именно эта граница делает обработку исключений полезной частью программы, а не способом спрятать traceback.


