W3docs

Python raise и пользовательские исключения

Узнайте, как использовать оператор raise в Python, цепочки исключений raise...from и создавать собственные классы исключений для удобной обработки ошибок.

Python позволяет не только перехватывать ошибки — вы также можете сигнализировать о них явно с помощью оператора raise и создавать собственные типы исключений для представления предметно-специфических проблем. Эта глава дополняет раздел Python Try...Except и охватывает:

  • Оператор raise — вызов встроенных исключений
  • Повторный вызов исключений внутри блока except
  • Цепочки исключений с raise ... from
  • Создание пользовательских классов исключений
  • Построение иерархии исключений для реального приложения
  • Оператор assert и случаи его применения

Оператор raise

Оператор raise позволяет вызвать исключение в любом месте вашего кода. Наиболее распространённая форма передаёт экземпляр исключения с описательным сообщением:

raise ExceptionType("message")

Используйте raise, когда ваш код обнаруживает проблему, которую должен обработать вызывающий код. Например, функция, принимающая возраст, должна немедленно отклонять отрицательные значения, а не продолжать работу молча:

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

try:
    set_age(-1)
except ValueError as e:
    print(e)
# Output: Age cannot be negative

Выбор подходящего встроенного исключения

Встроенные типы исключений Python несут смысловую нагрузку. Правильный выбор исключения делает ваш API более понятным и позволяет вызывающему коду обрабатывать разные категории ошибок по отдельности.

ИсключениеКогда вызывать
ValueErrorАргумент имеет правильный тип, но недопустимое значение (age = -1)
TypeErrorАргумент имеет неправильный тип (age = "old")
KeyErrorОтсутствует обязательный ключ словаря
IndexErrorИндекс последовательности выходит за границы допустимого диапазона
FileNotFoundErrorНеобходимый файл не существует
PermissionErrorПроцесс не имеет прав для выполнения операции
RuntimeErrorОбщая проблема во время выполнения, не подходящая под более конкретный тип
NotImplementedErrorМетод определён в базовом классе, но должен быть переопределён

Вызов ValueError при неверном значении значительно информативнее, чем вызов голого Exception, поскольку вызывающий код может написать except ValueError для обработки именно этого случая.

Повторный вызов исключения

Иногда нужно выполнить какое-то действие при возникновении исключения — записать его в лог, освободить ресурс — а затем позволить тому же исключению распространиться к вызывающему коду без изменений. Вызовите raise без аргументов внутри блока except, чтобы повторно вызвать текущее исключение:

def read_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        print(f"Warning: config file not found at {path}")
        raise  # re-raise the original FileNotFoundError

try:
    read_config("missing.cfg")
except FileNotFoundError as e:
    print(f"Caught: {e}")
# Output:
# Warning: config file not found at missing.cfg
# Caught: [Errno 2] No such file or directory: 'missing.cfg'

Голый raise сохраняет исходную трассировку стека, что делает отладку значительно проще, чем перехват и повторный вызов e в виде нового исключения.

Цепочки исключений с raise ... from

Когда вы перехватываете одно исключение и вызываете другое, Python автоматически записывает исходное исключение как контекст нового. Вы можете сделать эту связь явной и осмысленной с помощью raise NewException from original:

def load_data(path):
    try:
        with open(path) as f:
            return f.read()
    except OSError as e:
        raise RuntimeError("Failed to load configuration") from e

try:
    load_data("config.json")
except RuntimeError as e:
    print(f"Error: {e}")
    print(f"Caused by: {e.__cause__}")
# Output:
# Error: Failed to load configuration
# Caused by: [Errno 2] No such file or directory: 'config.json'

При выводе трассировки стека Python отображает оба исключения по порядку, чётко показывая, что RuntimeError стал прямым следствием OSError. Это особенно полезно в библиотечном коде, где нужно преобразовывать низкоуровневые ошибки ОС в высокоуровневые доменные ошибки, не скрывая первопричину.

Подавление цепочки с raise ... from None

Иногда исходное исключение является деталью реализации, которую не нужно раскрывать. Передайте None в качестве причины, чтобы скрыть его:

def fetch(url):
    try:
        raise ConnectionError("timeout")
    except ConnectionError:
        raise RuntimeError("Network unavailable") from None

try:
    fetch("http://example.com")
except RuntimeError as e:
    print(f"Error: {e}")
    print(f"Cause hidden: {e.__cause__}")
# Output:
# Error: Network unavailable
# Cause hidden: None

В трассировке будет показан только RuntimeError. Используйте этот приём редко — скрытие первопричины затрудняет отладку для пользователей библиотеки.

Создание пользовательских классов исключений

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

Пользовательское исключение — это просто класс, наследующийся от Exception (или одного из его подклассов):

class InsufficientFundsError(Exception):
    """Raised when a bank account has insufficient funds."""
    def __init__(self, amount, balance):
        self.amount = amount
        self.balance = balance
        super().__init__(
            f"Cannot withdraw {amount}: balance is only {balance}"
        )

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(amount, self.balance)
        self.balance -= amount
        return self.balance

account = BankAccount(100)
try:
    account.withdraw(150)
except InsufficientFundsError as e:
    print(e)
    print(f"You tried to withdraw: {e.amount}")
    print(f"Available balance:     {e.balance}")
# Output:
# Cannot withdraw 150: balance is only 100
# You tried to withdraw: 150
# Available balance:     100

Ключевые моменты этого паттерна:

  • super().__init__(message) задаёт удобочитаемую строку, возвращаемую str(e).
  • Дополнительные атрибуты (self.amount, self.balance) позволяют вызывающему коду получать структурированные данные из исключения, а не только строку.
  • Чёткая строка документации описывает, когда должно вызываться исключение.

Построение иерархии исключений

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

class AppError(Exception):
    """Base class for all application errors."""

class ValidationError(AppError):
    """Raised when user input fails validation."""

class DatabaseError(AppError):
    """Raised when a database operation fails."""

def validate_username(name):
    if len(name) < 3:
        raise ValidationError(f"Username '{name}' is too short (min 3 chars)")

try:
    validate_username("ab")
except ValidationError as e:
    print(f"Validation failed: {e}")
except AppError as e:
    print(f"Application error: {e}")
# Output:
# Validation failed: Username 'ab' is too short (min 3 chars)

Вызывающий код, которому нужно перехватывать только ошибки базы данных, может написать except DatabaseError. Вызывающий код, желающий перехватить любую проблему из вашей библиотеки, может написать except AppError. Это повторяет структуру собственной иерархии исключений Python, где OSError объединяет FileNotFoundError, PermissionError и несколько других.

Рекомендации по созданию пользовательских исключений

  • Наследуйтесь от Exception, а не от BaseException. BaseException — корень иерархии Python, включающий также SystemExit и KeyboardInterrupt, которые не должны перехватываться случайно.
  • Завершайте имя класса на Error для исключений, сигнализирующих о проблеме. Это соответствует соглашениям Python (ValueError, TypeError, IOError).
  • Держите класс минимальным, если не нужны дополнительные атрибуты. Пустое тело со строкой документации совершенно допустимо.
  • Помещайте исключения в отдельный модуль (например, exceptions.py) в крупных проектах, чтобы вызывающий код мог импортировать их, не затрагивая остальной код.

Оператор assert

assert — это лёгкий способ выражать инварианты — условия, которые должны быть истинными для корректной работы вашего кода:

def divide(a, b):
    assert b != 0, "Divisor must not be zero"
    return a / b

try:
    divide(10, 0)
except AssertionError as e:
    print(f"AssertionError: {e}")

print(divide(10, 2))
# Output:
# AssertionError: Divisor must not be zero
# 5.0

assert condition, message вызывает AssertionError с заданным сообщением, когда condition равно False.

Важное ограничение: Python удаляет операторы assert при запуске с флагом -O (оптимизация). Это означает:

  • Используйте assert только для проверки внутренней согласованности и вспомогательных средств отладки.
  • Используйте raise с подходящим исключением для проверки пользовательских входных данных и проверок публичного API, которые должны выполняться всегда.

Типичные ошибки

Перехват исключений с молчаливым игнорированием

# Bad — the error disappears
try:
    result = risky_operation()
except Exception:
    pass

# Better — at minimum, log or re-raise
try:
    result = risky_operation()
except Exception as e:
    print(f"Operation failed: {e}")
    raise

Вызов строки вместо исключения

# Wrong — strings are not exceptions
raise "something went wrong"  # TypeError

# Correct
raise ValueError("something went wrong")

Случайный перехват BaseException

# Dangerous — this catches KeyboardInterrupt and SystemExit too
except BaseException:
    ...

# Use Exception instead
except Exception:
    ...

Итоги

ТехникаКогда использовать
raise ExceptionType("msg")Сигнализировать о проблеме, которую должен обработать вызывающий код
raise (голый)Повторно вызвать текущее исключение после логирования или очистки
raise NewError(...) from originalПреобразовать низкоуровневую ошибку в высокоуровневую, сохраняя причину
raise NewError(...) from NoneПреобразовать ошибку, скрывая внутреннюю причину
Пользовательский класс исключенияДать доменным ошибкам уникальный, перехватываемый тип
Иерархия исключенийПозволить вызывающему коду перехватывать узкие или широкие категории ошибок
assertПроверять внутренние инварианты только в процессе разработки

Для полного понимания перехвата и обработки исключений см. Python Try...Except. Чтобы разобраться, как пользовательские исключения вписываются в проектирование классов, обратитесь к разделам Python Classes and Objects и Python Inheritance.

Практика

Практика
Which statement correctly raises a ValueError with the message 'invalid input'?
Which statement correctly raises a ValueError with the message 'invalid input'?
Практика
What does bare raise (with no argument) do inside an except block?
What does bare raise (with no argument) do inside an except block?
Практика
Which base class should a custom exception inherit from?
Which base class should a custom exception inherit from?
Was this page helpful?