Пользовательские исключения Python и цепочка raise from
Пользовательское исключение обозначает ошибку на языке предметной области. Вместо расплывчатого ValueError вызывающий код может получить OrderFormatError или PaymentRejectedError…
Пользовательское исключение обозначает ошибку на языке предметной области. Вместо расплывчатого ValueError вызывающий код может получить OrderFormatError или PaymentRejectedError и обработать именно ожидаемую ситуацию. Когда низкоуровневая ошибка преобразуется в доменную, конструкция raise NewError(...) from cause сохраняет причинную связь. В traceback остаются оба уровня: понятный интерфейс функции и техническая причина, полезная для диагностики.
Иерархия исключений как часть интерфейса
Прикладные исключения обычно наследуют от Exception, иногда через общий базовый класс подсистемы. Это позволяет клиенту ловить узкий тип или всю группу. Имена должны описывать сбой, а не место строки: ConfigError понятнее, чем ParserProblemAtLineError. Пустого класса часто достаточно; дополнительные поля нужны, если обработчику требуется структурированная информация, а не разбор текста сообщения.
Рабочий пример преобразования ошибки
class ConfigError(Exception):
pass
class InvalidPortError(ConfigError):
def __init__(self, raw_value: str) -> None:
self.raw_value = raw_value
super().__init__(f'некорректный порт: {raw_value!r}')
def parse_port(raw_value: str) -> int:
try:
port = int(raw_value)
except ValueError as exc:
raise InvalidPortError(raw_value) from exc
if not 1 <= port <= 65535:
raise InvalidPortError(raw_value)
return port
for raw in ['8080', 'eight']:
try:
print('порт:', parse_port(raw))
except ConfigError as exc:
print(type(exc).__name__, exc)Ожидаемый результат
порт: 8080
InvalidPortError некорректный порт: 'eight'В первом случае функция возвращает нормализованное число. Во втором обработчик видит доменный тип, а сохранённый атрибут raw_value доступен программе. Если не перехватывать исключение вокруг вызова, traceback покажет исходный ValueError, затем пояснение о том, что он был непосредственной причиной InvalidPortError.
Явная причина и подавление контекста
raise ... from exc устанавливает атрибут __cause__ и прямо сообщает о преобразовании. Если новое исключение возникает в обработчике без from, Python обычно сохраняет неявный __context__, но сообщение менее точно описывает намерение. Форма raise NewError(...) from None скрывает отображение предыдущего контекста; её следует применять осознанно, когда внутренняя ошибка действительно не помогает пользователю, а не ради короткого traceback.
Как не потерять исходную причину
Характерная ошибка — слишком широкий блок except Exception, который перехватывает и маскирует дефект внутри последующего кода. Ограничьте try выражением int(raw_value) и ловите ожидаемый ValueError. Если traceback показывает только пользовательское исключение без понятной причины, проверьте, не потеряна ли конструкция from exc и не применено ли from None.
Ещё одна проблема — обработчик, который печатает ошибку и продолжает работу с неинициализированным значением. Исключение либо превращают в корректный результат на границе приложения, либо дают ему подняться; простая печать не восстанавливает инвариант функции.
Практикум: ошибки импорта данных
Определите базовое ImportDataError и дочернее InvalidRowError с номером строки. Функция должна преобразовать ValueError при чтении целого количества в InvalidRowError через raise ... from exc. Отдельно отклоните отрицательное число без искусственной низкоуровневой причины. Самостоятельная проверка: корректная строка возвращает число, текст вызывает цепочку из двух исключений, отрицательное число даёт только доменную ошибку, а обработчик может прочитать номер строки из атрибута.
Контракт доменных исключений
Исключение — часть контракта функции. Собственный тип делает ожидаемую ошибку различимой, общий базовый класс группирует связанные случаи, а raise from объясняет переход между уровнями абстракции. Ловите только то, что способны содержательно обработать, и оставляйте блок try узким.
Вопросы о raise и цепочках исключений
Нужно ли добавлять данные в каждый класс исключения?
Нет. Добавляйте поля, только если вызывающий код будет принимать решения по ним. Для многих случаев достаточно имени класса и сообщения.
Чем повторный raise отличается от raise exc?
Одинокий raise внутри обработчика повторно поднимает текущее исключение с исходной трассировкой. Запись raise exc меняет точку, отображаемую в трассировке, и обычно не нужна.
Когда использовать from None?
Когда внутренний контекст намеренно не является частью пользовательской диагностики. Для журналов и отладки сохранённая причина чаще полезнее.