Создание собственных исключений для контроля - Python с нуля до Junior: Путь к первой работе в IT - Qpel.AI

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

В прошлом уроке мы научились ловить и обрабатывать стандартные ошибки Python с помощью try-except. Это мощный механизм, но иногда его не хватает. Что делать, если нужно сообщить о проблеме, которая не вписывается в существующие типы ошибок? Именно для таких случаев Python позволяет создавать свои исключения.

Зачем нужны свои исключения?

Представьте: вы пишете код для онлайн-магазина. Пользователь хочет купить товар, которого нет в наличии. Это не ошибка программы в привычном смысле (не синтаксис, не логика), но это исключительная ситуация, которую нужно обработать. Стандартные ValueError или TypeError не подходят для описания "товара нет на складе".

Свои исключения помогают:

  • Сделать код понятнее. Имя исключения сразу говорит, что случилось.
  • Разделить логику. Вы ловите конкретные ошибки, не затрагивая другие.
  • Ускорить отладку. Понятные исключения быстро приводят к источнику проблемы.
  • Строить иерархии. Вы можете создавать логическую структуру ошибок, наследуя их друг от друга.

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

В Python все исключения — это классы, которые наследуются от базового класса Exception (или его подклассов, например, ValueError, TypeError). Чтобы создать своё исключение, просто объявите новый класс, который наследуется от Exception.

class InsufficientStockError(Exception):
    """
    Исключение: товара нет на складе или его недостаточно.
    """
    def __init__(self, item_name, requested_quantity, available_quantity):
        self.item_name = item_name
        self.requested_quantity = requested_quantity
        self.available_quantity = available_quantity
        message = (f"Недостаточно товара '{item_name}'. "
                   f"Запрошено: {requested_quantity}, в наличии: {available_quantity}.")
        super().__init__(message) # Передаём сообщение родительскому классу

# Пример использования
def process_order(item, quantity):
    # Допустим, это данные из базы данных
    available_stock = {"Книга": 10, "Ручка": 5, "Тетрадь": 2} 

    if item not in available_stock:
        raise ValueError(f"Товар '{item}' не найден.")
    
    if quantity > available_stock[item]:
        # Вызываем наше кастомное исключение
        raise InsufficientStockError(item, quantity, available_stock[item])
    
    print(f"Заказ на {quantity} шт. товара '{item}' успешно обработан.")

# Демонстрация работы
try:
    process_order("Книга", 12) # Вызовет InsufficientStockError
except InsufficientStockError as e:
    print(f"Ошибка заказа: {e}")
    print(f"Детали: Товар '{e.item_name}', запрошено {e.requested_quantity}, в наличии {e.available_quantity}")
except ValueError as e:
    print(f"Ошибка данных: {e}")
except Exception as e: # Ловим любые другие неожиданные ошибки
    print(f"Произошла непредвиденная ошибка: {e}")

print("\n--- Второй пример ---")
try:
    process_order("Ручка", 3) # Успешный заказ
    process_order("Карандаш", 1) # Вызовет ValueError
except InsufficientStockError as e:
    print(f"Ошибка заказа: {e}")
except ValueError as e:
    print(f"Ошибка данных: {e}")

Что мы сделали в этом примере:

  1. Создали класс InsufficientStockError, который наследуется от Exception.
  2. В его конструкторе __init__ добавили параметры (item_name, requested_quantity, available_quantity), чтобы сообщение об ошибке было максимально информативным.
  3. Вызвали конструктор родительского класса super().__init__(message), чтобы передать ему стандартное сообщение об ошибке.
  4. Функция process_order "выбрасывает" (raise) наше кастомное исключение, если товара не хватает.
  5. Блок try-except ловит InsufficientStockError и ValueError по отдельности, позволяя нам обрабатывать каждую ситуацию по-своему.

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

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

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

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

class InvalidInputError(BusinessLogicError):
    """Исключение: некорректный ввод данных."""
    def __init__(self, message="Некорректный ввод данных", field=None):
        super().__init__(message)
        self.field = field

class PermissionDeniedError(BusinessLogicError):
    """Исключение: ошибка доступа."""
    def __init__(self, user_role, required_role):
        message = f"Доступ запрещен. Ваша роль: {user_role}, требуется: {required_role}."
        super().__init__(message)
        self.user_role = user_role
        self.required_role = required_role

def check_access(user_role, resource):
    if resource == "секретный_отчет" and user_role != "админ":
        raise PermissionDeniedError(user_role, "админ")
    print(f"Доступ к {resource} разрешен для {user_role}.")

def validate_data(data):
    if not isinstance(data, dict):
        raise InvalidInputError("Данные должны быть словарем.")
    if "name" not in data or not data["name"]:
        raise InvalidInputError("Поле 'name' обязательно.", field="name")
    print("Данные валидны.")

try:
    check_access("пользователь", "секретный_отчет")
except PermissionDeniedError as e:
    print(f"Ошибка доступа: {e.message} (роль пользователя: {e.user_role})")
except BusinessLogicError as e: # Можно поймать любое исключение из нашей иерархии
    print(f"Общая ошибка бизнес-логики: {e}")

print("\n--- Второй пример ---")
try:
    validate_data({"age": 30})
except InvalidInputError as e:
    print(f"Ошибка валидации: {e.message}")
    if e.field:
        print(f"Проблема в поле: {e.field}")
except BusinessLogicError as e:
    print(f"Общая ошибка бизнес-логики: {e}")

Здесь мы можем поймать PermissionDeniedError или InvalidInputError по отдельности. А можем поймать их все разом, перехватив BusinessLogicError, так как они являются его подклассами. Это даёт большую гибкость в обработке ошибок.

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

В следующем разделе мы перейдём к одной из ключевых тем для любого разработчика — основам работы с системой контроля версий Git. Без этого инструмента невозможно представить современную командную разработку.