Создание своих исключений: классы исключений

Создание своих исключений: классы исключений Обработка исключений

Привет! Ты уже знаешь, как вручную вызывать исключения с помощью raise и использовать встроенные типы ошибок. Но что если ни одно встроенное исключение не описывает твою ситуацию? Например, в твоей программе может быть ошибка «Недостаточно средств на счете» или «Пользователь не найден». Для таких случаев Python позволяет создавать собственные исключения.

В этой статье мы разберём:

  • Зачем создавать свои исключения
  • Как создать класс исключения
  • Как добавить атрибуты для полезной информации
  • Как использовать свои исключения в коде
  • Иерархия собственных исключений

Что нужно знать перед началом

Для этого урока тебе понадобится:

  • Установленный Python
  • Базовое понимание классов в Python
  • Знание исключений и конструкции try-except

Совет: Создание своих исключений — это способ сделать код более выразительным и понятным. Когда ты читаешь raise InsufficientFundsError, сразу понятно, что произошло.

Основная часть

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

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

raise InsufficientFundsError('На счете недостаточно средств')

Ты сразу понимаешь, что произошло именно это, а не что-то другое.

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

Синтаксис создания исключений

Создать своё исключение — это создать класс, который наследуется от Exception (или другого встроенного исключения).

class MyCustomError(Exception):
    """Моё собственное исключение."""
    pass

Совет: Документируй свои исключения (docstring). Это помогает другим разработчикам понять, когда использовать это исключение.

Простейшее собственное исключение

class NegativeAgeError(Exception):
    """Исключение для отрицательного возраста."""
    pass

def set_age(age):
    if age < 0:
        raise NegativeAgeError(f'Возраст не может быть отрицательным: {age}')
    print(f'Возраст установлен: {age}')

# Использование
try:
    set_age(-5)
except NegativeAgeError as e:
    print(f'Ошибка: {e}')

Результат:

Ошибка: Возраст не может быть отрицательным: -5

Совет: Названия своих исключений принято заканчивать на Error или Exception.

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

Можно создавать целую иерархию исключений для разных типов ошибок.

class ValidationError(Exception):
    """Базовое исключение для ошибок валидации."""
    pass

class NegativeAgeError(ValidationError):
    """Ошибка: возраст отрицательный."""
    pass

class InvalidEmailError(ValidationError):
    """Ошибка: невалидный email."""
    pass

class PasswordTooShortError(ValidationError):
    """Ошибка: пароль слишком короткий."""
    pass

Использование иерархии

def validate_user(name, age, email, password):
    if not name:
        raise ValidationError('Имя не может быть пустым')
    if age < 0:
        raise NegativeAgeError(f'Возраст {age} отрицательный')
    if '@' not in email:
        raise InvalidEmailError(f'Email {email} невалидный')
    if len(password) < 6:
        raise PasswordTooShortError('Пароль должен быть минимум 6 символов')
    print('Пользователь валиден')

# Можно перехватывать все ошибки валидации
try:
    validate_user('Анна', -5, 'anna@mail.ru', '12345')
except ValidationError as e:
    print(f'Ошибка валидации: {e}')

Результат:

Ошибка валидации: Возраст -5 отрицательный

Совет: Иерархия позволяет перехватывать все ошибки одного типа (ValidationError) или только конкретные (NegativeAgeError).

Добавление атрибутов в исключения

Иногда нужно передать больше информации об ошибке: код ошибки, поле, в котором произошла ошибка, или исходные данные.

class ValidationError(Exception):
    def __init__(self, message, field=None, code=None):
        super().__init__(message)
        self.field = field
        self.code = code

def validate_age(age):
    if age < 0:
        raise ValidationError(
            'Возраст не может быть отрицательным',
            field='age',
            code='NEGATIVE_AGE'
        )
    if age > 150:
        raise ValidationError(
            'Нереалистичный возраст',
            field='age',
            code='UNREALISTIC_AGE'
        )
    print(f'Возраст {age} валиден')

try:
    validate_age(-10)
except ValidationError as e:
    print(f'Ошибка: {e}')
    print(f'Поле: {e.field}')
    print(f'Код: {e.code}')

Результат:

Ошибка: Возраст не может быть отрицательным
Поле: age
Код: NEGATIVE_AGE

Совет: Атрибуты field и code очень полезны для логирования и для фронтенда, где нужно показать ошибку рядом с конкретным полем.

Создание исключений для бизнес-логики

class AccountError(Exception):
    """Базовое исключение для операций со счётом."""
    pass

class InsufficientFundsError(AccountError):
    """Ошибка: недостаточно средств."""
    def __init__(self, balance, amount):
        super().__init__(f'Недостаточно средств: баланс {balance}, запрошено {amount}')
        self.balance = balance
        self.amount = amount

class AccountNotFoundError(AccountError):
    """Ошибка: аккаунт не найден."""
    def __init__(self, account_id):
        super().__init__(f'Аккаунт {account_id} не найден')
        self.account_id = account_id

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

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        print(f'Снято {amount}. Остаток: {self.balance}')

# Использование
accounts = {1: BankAccount(1, 100)}

def get_account(account_id):
    if account_id not in accounts:
        raise AccountNotFoundError(account_id)
    return accounts[account_id]

try:
    account = get_account(2)
    account.withdraw(200)
except AccountNotFoundError as e:
    print(f'Ошибка: {e}')
except InsufficientFundsError as e:
    print(f'Ошибка: {e}')

Переопределение стандартных методов

Можно переопределить __str__ или __repr__ для более красивой строки ошибки.

class MyError(Exception):
    def __init__(self, message, code):
        self.message = message
        self.code = code

    def __str__(self):
        return f'[{self.code}] {self.message}'

    def __repr__(self):
        return f'MyError(code={self.code}, message={self.message})'

try:
    raise MyError('Что-то пошло не так', 404)
except MyError as e:
    print(e)      # [404] Что-то пошло не так
    print(repr(e)) # MyError(code=404, message=Что-то пошло не так)

Совет: Переопределяй __str__ для удобного вывода сообщения. Это полезно для логирования.

Использование с другими механизмами

Связывание с вызывающим кодом

class CustomError(Exception):
    pass

def process_data(data):
    try:
        # Какой-то код с ошибкой
        result = int(data)
    except ValueError as e:
        # Связываем с нашим исключением
        raise CustomError('Не удалось преобразовать данные') from e

try:
    process_data('abc')
except CustomError as e:
    print(f'Ошибка: {e}')
    print(f'Причина: {e.__cause__}')

Результат:

Ошибка: Не удалось преобразовать данные
Причина: invalid literal for int() with base 10: 'abc'

Группировка исключений

try:
    # Код, который может вызвать разные ошибки
    validate_user(name, age, email, password)
except (ValidationError, ValueError) as e:
    print(f'Ошибка ввода: {e}')

Полный пример: банковское приложение

import logging

class BankError(Exception):
    """Базовое исключение банка."""
    pass

class InsufficientFundsError(BankError):
    def __init__(self, balance, amount):
        super().__init__(f'Недостаточно средств: баланс {balance}, запрошено {amount}')
        self.balance = balance
        self.amount = amount

class AccountNotFoundError(BankError):
    def __init__(self, account_id):
        super().__init__(f'Аккаунт {account_id} не найден')
        self.account_id = account_id

class InvalidTransactionError(BankError):
    def __init__(self, message, transaction_id):
        super().__init__(message)
        self.transaction_id = transaction_id

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

    def deposit(self, amount):
        if amount <= 0:
            raise InvalidTransactionError('Сумма депозита должна быть положительной', 'deposit')
        self.balance += amount
        print(f'Внесено {amount}. Баланс: {self.balance}')

    def withdraw(self, amount):
        if amount <= 0:
            raise InvalidTransactionError('Сумма снятия должна быть положительной', 'withdraw')
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance -= amount
        print(f'Снято {amount}. Баланс: {self.balance}')

# Логирование ошибок
logging.basicConfig(level=logging.ERROR)

def handle_bank_operation():
    accounts = {'001': BankAccount('001', 100)}
    try:
        account = accounts['002']  # Ошибка!
        account.withdraw(200)
    except AccountNotFoundError as e:
        logging.error(f'Аккаунт не найден: {e}')
    except InsufficientFundsError as e:
        logging.error(f'Недостаточно средств: {e}')
    except InvalidTransactionError as e:
        logging.error(f'Ошибка транзакции {e.transaction_id}: {e}')
    except BankError as e:
        logging.error(f'Общая банковская ошибка: {e}')
    except Exception as e:
        logging.error(f'Неизвестная ошибка: {e}')

handle_bank_operation()

Задачи для закрепления

Задача 1. Создай класс исключения NegativeValueError, который выбрасывается при отрицательном значении.

Задача 2. Создай исключение UserNotFoundError с атрибутом user_id.

Задача 3. Создай иерархию исключений для валидации: ValidationError → EmptyFieldError и InvalidFormatError.

Задача 4. Добавь собственное исключение в функцию divide(a, b) для деления на ноль.

Задача 5. Используй raise ... from для связывания своего исключения с встроенным.

Ответы:

Задача 1.

class NegativeValueError(Exception):
    pass

def check_value(x):
    if x < 0:
        raise NegativeValueError(f'Значение {x} отрицательное')

Задача 2.

class UserNotFoundError(Exception):
    def __init__(self, user_id):
        super().__init__(f'Пользователь {user_id} не найден')
        self.user_id = user_id

def get_user(user_id):
    # ... если не найден ...
    raise UserNotFoundError(user_id)

Задача 3.

class ValidationError(Exception):
    pass

class EmptyFieldError(ValidationError):
    def __init__(self, field):
        super().__init__(f'Поле {field} пустое')
        self.field = field

class InvalidFormatError(ValidationError):
    def __init__(self, field, format_example):
        super().__init__(f'Поле {field} имеет неверный формат. Пример: {format_example}')
        self.field = field
        self.format_example = format_example

Задача 4.

class DivisionByZeroError(Exception):
    pass

def divide(a, b):
    if b == 0:
        raise DivisionByZeroError('На ноль делить нельзя')
    return a / b

Задача 5.

class MyError(Exception):
    pass

try:
    int('abc')
except ValueError as e:
    raise MyError('Ошибка преобразования') from e

Нюансы и подводные камни

Класс исключения должен наследовать Exception

# Неправильно
class MyError:
    pass

# Правильно
class MyError(Exception):
    pass

Названия исключений

Принято использовать суффиксы Error или Exception для наглядности.

Документация

Всегда добавляй docstring к своему классу исключения — это помогает другим разработчикам.

Частые ошибки и как их избежать

Ошибка 1: Слишком общий except

# Перехватываем все ошибки
except Exception as e:
    print(e)

# Перехватываем конкретное исключение
except MyCustomError as e:
    print(f'Ошибка: {e}')

Ошибка 2: Забыл super() в init

class MyError(Exception):
    def __init__(self, message, code):
        self.code = code
# Забыли super()

# Должно быть
        super().__init__(message)
        self.code = code

Ошибка 3: Создание исключения, но не вызов

# Неправильно
try:
    MyError('Ошибка')
except MyError:
    pass

# Правильно
try:
    raise MyError('Ошибка')
except MyError:
    pass

Шпаргалка

Что нужноКак пишется
Простейшее исключениеclass MyError(Exception): pass
С атрибутамиclass MyError(Exception): def __init__(self, msg): super().__init__(msg); self.extra = extra
Иерархияclass ChildError(ParentError): pass
Поднять исключениеraise MyError('Ошибка')
Перехватитьexcept MyError as e:
Связать с причинойraise MyError('...') from e

Заключение

Сегодня мы научились:

  • Зачем создавать свои исключения
  • Как создать класс исключения
  • Создавать иерархии исключений
  • Добавлять атрибуты для полезной информации
  • Связывать исключения с помощью from

КВИЗ

Что дальше?

Оцените статью
IMI-DS - PYTHON LERNEN
Содержание
Оглавление ×