📖 Теория: Исключения и обработка ошибок

⚡ Кратко

Исключения — это объекты, которые Python создаёт при ошибке. Если их не обработать, программа завершается и печатает traceback.

Конструкция try/except позволяет перехватить ошибку и продолжить работу. Обработчики проверяются сверху вниз, поэтому общий Exception ставят в конец.

else выполняется при успехе, finally — всегда. raise возбуждает исключение, а logging помогает записывать ошибки в файл.

Лучшие практики: ловите конкретные исключения, не подавляйте ошибки, используйте finally или менеджеры контекста для ресурсов.

Что такое исключения

Исключение (exception) — это событие, которое возникает во время выполнения программы и сигнализирует об ошибочной ситуации. В Python исключение — это объект, содержащий:

  • тип — класс исключения, например ValueError;
  • сообщение — описание проблемы;
  • traceback — информацию о том, где именно произошла ошибка.

Если исключение не обработано, интерпретатор останавливает программу и выводит traceback:

# zero_division_demo.py
print(10 / 0)  # ZeroDivisionError: division by zero

Обработка исключений позволяет заменить аварийное завершение на понятное сообщение и продолжить выполнение.

Частые встроенные исключения

ИсключениеПричинаПример
ZeroDivisionError Деление на ноль 10 / 0
ValueError Некорректное значение int("abc")
KeyError Несуществующий ключ словаря info = {"a": 1}; info["b"]
IndexError Индекс за пределами последовательности [1, 2][10]
TypeError Операция для неподходящего типа "2" + 2
FileNotFoundError Файл не найден open("missing.txt")

Иерархия исключений

Все исключения в Python наследуются от BaseException. Однако для обработки ошибок обычно используют ветвь Exception:

BaseException
 ├── SystemExit          # выход из программы
 ├── KeyboardInterrupt   # Ctrl+C
 ├── GeneratorExit       # закрытие генератора
 └── Exception           # базовый класс для ошибок
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── IndexError
      │    └── KeyError
      ├── ValueError
      ├── TypeError
      └── ...

Общий обработчик except Exception: перехватывает все ошибки из ветви Exception, но не прерывает программу при Ctrl+C или sys.exit().

Конструкция try/except

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

# safe_divide.py
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Ошибка: деление на ноль!")

print("Программа продолжает работу")

Важные правила:

  • В блок try включают только код, который может вызвать исключение. Это упрощает отладку.
  • Обработчики проверяются сверху вниз. Первый подходящий сработает, остальные будут пропущены.
  • Общий except Exception: ставят последним, иначе он перехватит всё и более специфичные обработчики не сработают.

Несколько исключений

Можно задать несколько обработчиков или поймать несколько типов в одном блоке через кортеж:

# multiple_exceptions.py
while True:
    try:
        value = int(input("Введите ненулевое число: "))
        result = 10 / value
        print(f"Результат: {result}")
        break
    except (ZeroDivisionError, ValueError):
        print("Ошибка! Введите корректное ненулевое число.")

Для получения текста ошибки используют конструкцию as e:

# exception_message.py
try:
    number = int("abc")
except ValueError as e:
    print(f"Произошла ошибка: {e}")

Блоки else и finally

else выполняется, если в try не возникло исключения. Он помогает отделить «опасный» код от основной логики:

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

finally выполняется в любом случае — независимо от того, было исключение или нет. Его используют для освобождения ресурсов:

# try_finally.py
try:
    number = int(input("Введите число: "))
    result = 10 / number
except (ValueError, ZeroDivisionError):
    print("Ошибка ввода или деления.")
finally:
    print("Завершение программы.")

Полная конструкция выглядит так:

# full_structure.py
try:
    number = int(input("Введите число: "))
    result = 10 / number
except ValueError:
    print("Некорректное значение.")
except ZeroDivisionError:
    print("Деление на ноль.")
else:
    print(f"Результат: {result}")
finally:
    print("Программа завершена.")

Возбуждение исключений через raise

Ключевое слово raise позволяет программисту самому создать исключение. Это полезно, когда внешние данные нарушают бизнес-правила:

# raise_demo.py
age = int(input("Введите возраст: "))
if age < 18:
    raise ValueError("Возраст должен быть 18 лет и старше.")

После raise программа останавливается, если исключение не перехвачено:

# raise_with_except.py
while True:
    try:
        age = int(input("Введите возраст: "))
        if age < 18:
            raise ValueError("Возраст должен быть 18 лет и старше.")
        print("Возраст принят.")
        break
    except ValueError as e:
        print(f"Ошибка: {e}. Попробуйте снова.")

Пользовательские исключения

Собственные исключения создаются наследованием от Exception (или его подкласса). Это делает ошибки программы более понятными и позволяет отличать их от встроенных:

# custom_exception.py
class AgeValidationError(Exception):
    """Ошибка: пользователь слишком молод."""

    def __init__(self, age: int, min_age: int):
        self.age = age
        self.min_age = min_age
        super().__init__(f"Возраст {age} меньше минимального {min_age}")


def check_age(age: int) -> None:
    if age < 18:
        raise AgeValidationError(age, 18)
    print("Доступ разрешён.")


try:
    check_age(15)
except AgeValidationError as e:
    print(e)

От какого класса наследовать

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

РодительКогда выбирать
ValueErrorзначение правильного типа, но недопустимое по смыслу
TypeErrorполучен неподходящий тип данных
LookupErrorошибка поиска (общий предок KeyError и IndexError)
KeyErrorотсутствует ключ
RuntimeErrorнедопустимое состояние программы
Exceptionподходящего типа нет — общий вариант
⚠️ Не наследуйте от BaseException. От него происходят служебные сигналы KeyboardInterrupt (Ctrl+C) и SystemExit. Пользовательские ошибки наследуют от Exception или его потомков — иначе обычный except Exception перестанет их ловить.

raise и except решают разные задачи

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

def validate_age(age: int) -> None:
    if age < 0:
        raise ValueError("Возраст не может быть отрицательным")   # сообщаем о проблеме


try:
    validate_age(-5)
except ValueError as error:                                        # обрабатываем проблему
    print(f"Ошибка ввода: {error}")

Функция, обнаружившая проблему, обычно не знает, как её исправить, — поэтому она сообщает о ней через raise. Решение принимает вызывающий код в except.

⚠️ Проверить по документации: пользовательские исключения и переопределение __init__ упоминаются в требованиях к уроку, но в исходной лекции не рассматриваются подробно. Официальная документация рекомендует наследовать пользовательские исключения от Exception или его потомков.

Проверки через assert

Инструкция assert проверяет условие и возбуждает AssertionError, если оно ложно. Она используется для внутренних инвариантов и отладки, а не для проверки пользовательского ввода:

# assert_demo.py
def factorial(n: int) -> int:
    assert n >= 0, "n must be non-negative"
    if n == 0 or n == 1:
        return 1
    return n * factorial(n - 1)


print(factorial(5))  # 120
# factorial(-1)      # AssertionError
⚠️ Проверить по документации: assert и AssertionError не входят в исходную лекцию, но являются стандартным инструментом Python. При запуске с флагом -O (оптимизация) assert-инструкции игнорируются, поэтому для проверки внешних данных лучше использовать raise.

Логирование ошибок

Модуль logging позволяет записывать сообщения разного уровня важности. В отличие от print, логи можно направить в файл, добавить время, уровень и строку кода.

УровеньМетодКогда использовать
DEBUGlogging.debugОтладочная информация
INFOlogging.infoОбщая информация о работе
WARNINGlogging.warningПотенциальные проблемы
ERRORlogging.errorОшибки, программа продолжает работу
CRITICALlogging.criticalКритические ошибки

По умолчанию logging выводит только сообщения уровня WARNING и выше. Чтобы видеть DEBUG и INFO, нужно настроить уровень:

# logging_levels.py
import logging

logging.basicConfig(level=logging.DEBUG)

logging.debug("Отладочное сообщение")
logging.info("Информационное сообщение")
logging.warning("Предупреждение")
logging.error("Ошибка")
logging.critical("Критическая ошибка")

Запись в файл и настройка формата:

# logging_file.py
import logging

logging.basicConfig(
    filename="app.log",
    format="%(asctime)s - %(filename)s - %(lineno)d - %(levelname)s - %(message)s",
    level=logging.ERROR,
)

try:
    result = 10 / 0
except ZeroDivisionError as e:
    logging.error(f"Ошибка при делении: {e}")

Лучшие практики

  • Ловите конкретные исключения. «Голый» except: или except Exception: перехватывают слишком много, включая KeyboardInterrupt (если используется except BaseException) и мешают отладке.
  • Минимизируйте код в блоке try. Туда помещают только строки, которые действительно могут вызвать ожидаемое исключение.
  • Не подавляйте ошибки пустым pass. Если ошибка произошла, либо обработайте её, либо залогируйте.
  • Используйте finally для очистки. Закрывайте файлы, соединения и освобождайте ресурсы в любом случае.
  • Логируйте ошибки. В production важна история ошибок, а не только сообщение в консоль.
⚠️ Проверить по документации: для работы с файлами и другими ресурсами в современном Python рекомендуется использовать менеджеры контекста with. Они автоматически освобождают ресурсы даже при исключении и часто заменяют try/finally.

Краткий итог

Исключения — нормальная часть работы программы. Грамотная обработка ошибок делает код устойчивым: программа не падает от любого некорректного ввода, а сообщает о проблеме понятным способом и продолжает работу. Запомните главное: ловите конкретные ошибки, не подавляйте их и логируйте.