Исключения Python: как избежать сбоев

Раздел: Основы Python -> Отладка и исключения

Основы обработки исключений

Главный механизм

Для перехвата и обработки ошибок в Python применяется конструкция tryexcept. Она позволяет выполнить потенциально опасный код и, в случае возникновения исключения, перейти к заранее подготовленному блоку обработки.

try:
    user_input = input("Введите число: ")
    number = int(user_input)
    print(f"Вы ввели {number}")
except ValueError:
    print("Ошибка: введено не число")

ошибки в python (ошибки в python)

В этом примере блок try содержит преобразование строки в число. Если пользователь введёт нечисловое значение, возникает ValueError, и выполнение переходит к except. Без обработки программа завершилась бы аварийно.

Типичная проблема

Новички часто оборачивают в try слишком много кода, что затрудняет поиск источника ошибки. Рекомендуется помещать в блок только те строки, которые могут вызвать конкретное исключение.

Как обработать несколько типов исключений?

Если блок может породить разные исключения, можно перечислить их в кортеже или использовать отдельные except.

try:
    value = int(input())
    result = 10 / value
except ValueError:
    print("Нужно число")
except ZeroDivisionError:
    print("Деление на ноль")

Проблема

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

Как выполнить код независимо от исключения?

Блок finally выполняется всегда, даже если в try произошёл выход через return или break. Это удобно для закрытия файлов или освобождения ресурсов.

file = None
try:
    file = open("data.txt")
    content = file.read()
except FileNotFoundError:
    print("Файл не найден")
finally:
    if file:
        file.close()

Ошибка

В finally не рекомендуется размещать операции, которые могут снова вызвать исключение, иначе оно перекроет предыдущее. Если это необходимо, внутри finally используют вложенный try-except.

Как добавить действие при успешном выполнении?

Блок else выполняется, если в try не возникло исключения. Это помогает отделить основной сценарий от аварийного.

try:
    number = int(input())
except ValueError:
    print("Ошибка преобразования")
else:
    print(f"Введено число {number}")

Замечание

Код в else не выполняется, если произошёл выход через return, break или continue внутри блока try. Это редкий случай, но его стоит учитывать.

Как создать собственное исключение?

Иногда нужно описать ошибку, специфичную для приложения. Для этого создают класс, наследующий от Exception.

class NegativeValueError(Exception):
    """Исключение для отрицательных чисел."""
    pass

def check_positive(value):
    if value < 0:
        raise NegativeValueError("Значение не может быть отрицательным")
    return value

try:
    check_positive(-5)
except NegativeValueError as e:
    print(f"Ошибка: {e}")

Проблема

Собственные исключения должны быть документированы, иначе другим разработчикам будет непонятно, когда и почему они возникают. Также не стоит излишне плодить классы, если достаточно стандартных встроенных исключений.

Расширенные примеры работы с исключениями

Цепочка исключений (chained exceptions)

В Python 3 можно сохранять контекст исходной ошибки с помощью raise ... from. Это помогает отследить первопричину.

Пример
def read_file(filename):
    try:
        with open(filename) as f:
            return f.read()
    except FileNotFoundError:
        raise ValueError("Невозможно прочитать конфигурацию") from None  # отключаем цепочку

try:
    read_file("missing.cfg")
except ValueError as e:
    print(e)
    print("Скрытый контекст не показан, так как использовано from None")
Невозможно прочитать конфигурацию
Скрытый контекст не показан, так как использовано from None

Повторный вызов исключения (re-raise)

Иногда нужно частично обработать ошибку, а затем передать её дальше. Для этого применяют raise без аргументов внутри блока except.

Пример
def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("Попытка деления на ноль")
        raise  # повторно возбуждает то же исключение

try:
    safe_divide(10, 0)
except ZeroDivisionError:
    print("Обработано на верхнем уровне")
Попытка деления на ноль
Обработано на верхнем уровне

Логирование исключений с traceback

Вместо простого вывода ошибки в консоль лучше использовать модуль logging, который записывает полную трассу стека.

Пример
import logging

logging.basicConfig(level=logging.ERROR)

try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Произошло деление на ноль")
ERROR:root:Произошло деление на ноль
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

Использование assert для отладки

Оператор assert проверяет условие и возбуждает AssertionError при его ложности. Он удобен для самопроверок в коде, но отключается при запуске с оптимизацией (-O).

Пример
def factorial(n):
    assert n >= 0, "Факториал определён только для неотрицательных чисел"
    if n == 0:
        return 1
    return n * factorial(n - 1)

print(factorial(5))
print(factorial(-1))  # вызовет AssertionError
120
Traceback (most recent call last):
  ...
AssertionError: Факториал определён только для неотрицательных чисел

Фильтрация предупреждений (warnings)

Некоторые ошибки не являются критическими, но заслуживают внимания. warnings позволяет управлять их отображением.

Пример
import warnings
warnings.warn("Эта функция устарела", DeprecationWarning)

# Для игнорирования предупреждений:
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.warn("Это сообщение не будет показано", DeprecationWarning)
(при первом вызове вывод предупреждения, при втором – ничего)

Ошибки в Python - comments

En
ошибки в python (python)