Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 10 из 20 Средний 15 мин 100 XP

Свои исключения в Python: класс от Exception

ValueError говорит «что-то не то», но не «какое правило нарушено». Заводим свои классы исключений: внятное имя в логах, свои атрибуты и целые семейства ошибок.

Редакция Питоники

Седьмой урок научил нарушать правила через raise ValueError. У встроенного ValueError есть слабость: он говорит «значение не подошло», но не говорит, какое правило нарушено. В логе с десятью ValueError не разберёшься, что именно пошло не так: не хватило денег, не тот формат заказа или цена вышла за потолок. Для своих правил Python позволяет завести свои исключения — классы с внятными именами, которые попадают в traceback и читаются как заголовки событий.

Первое своё исключение: имя, которое читается

Минимальное своё исключение — класс от Exception с телом из одной строки. Имя класса придумываете вы, и это имя видно всем, кто читает traceback:

своё исключение без обработки
class InsufficientFundsError(Exception):
    """На счёте недостаточно денег для операции."""

balance = 100
withdraw = 250

if withdraw > balance:
    raise InsufficientFundsError(f'нужно {withdraw}, на счёте {balance}')
Никто не поймал — программа падает, и в последней строке traceback стоит: InsufficientFundsError: нужно 250, на счёте 100. Сравните с безликим ValueError: invalid literal for int()... — имя сразу говорит, какое правило нарушено.

Последняя строка traceback читается как заголовок в журнале: тип ошибки — имя вашего класса, далее — сообщение, которое вы передали в скобках. Строка в теле класса — docstring, она объясняет смысл исключения тем, кто будет читать ваш код через полгода. Никаких других методов для минимального варианта не нужно.

Ловим своё как любое другое

Своё исключение — полноправный член системы: бросается обычным raise, ловится обычным except, а сообщение читается через as e:

своя ошибка — своя ветка ловли
class InsufficientFundsError(Exception):
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(f'нужно {amount}, на счёте {balance}')
    return balance - amount

try:
    withdraw(100, 250)
except InsufficientFundsError as e:
    print('операция отменена:', str(e))
Вывод
операция отменена: нужно 250, на счёте 100

Ловится строго по имени: ветка except InsufficientFundsError сработает только на ваше исключение, и случайный ValueError из другого места кода в неё не попадёт. Побочный бонус такой точности: имя класса — это документация. Читая заголовок функции, видно, какие правила она может нарушить.

Исключение — это обычный класс, и в нём можно хранить данные о случившемся: номер заказа, имя файла, саму причину. Для этого переопределяют __init__, не забывая позвать родительский через super():

исключение с атрибутами
class PaymentError(Exception):
    def __init__(self, order_id, reason):
        super().__init__(reason)
        self.order_id = order_id
        self.reason = reason

try:
    raise PaymentError('ORD-7', 'карта отклонена')
except PaymentError as e:
    print('заказ', e.order_id, '-', e.reason)
Вывод
заказ ORD-7 - карта отклонена

Вызывающий код теперь отвечает на вопрос «что с заказом?» без разбора строк: атрибуты order_id и reason лежат в объекте ошибки. Сообщение в super().__init__(reason) идёт в стандартное поле, откуда его читает str(e) — и traceback, и журналы показывают осмысленный текст.

Семейство своих исключений

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

семейство ошибок магазина
class ShopError(Exception):
    """Общая ошибка магазина."""

class PaymentError(ShopError):
    pass

class DeliveryError(ShopError):
    pass

def process(order):
    raise DeliveryError('курьер недоступен')

try:
    process('ORD-1')
except ShopError as e:
    print('магазин сообщил:', type(e).__name__, '-', str(e))
Вывод
магазин сообщил: DeliveryError - курьер недоступен
родство проверяется как у встроенных
class ShopError(Exception):
    pass

class PaymentError(ShopError):
    pass

print('PaymentError - потомок ShopError:', issubclass(PaymentError, ShopError))
print('PaymentError - потомок Exception:', issubclass(PaymentError, Exception))
Вывод
PaymentError - потомок ShopError: True
PaymentError - потомок Exception: True

Всё, что вы знали о встроенной иерархии, работает и для своих классов: ловля предка ShopError перехватывает и PaymentError, и DeliveryError, а порядок веток «ребёнок раньше родителя» остаётся в силе. Практический смысл — один обработчик «на все ошибки магазина» на верхнем уровне программы и точные ветки для отдельных случаев ниже.

своё исключение поверх встроенного
class AgeError(Exception):
    pass

def parse_age(text):
    value = int(text)
    if value < 0 or value > 130:
        raise AgeError(f'возраст вне диапазона: {value}')
    return value

for text in ['25', '300']:
    try:
        print(text, '->', parse_age(text))
    except AgeError as e:
        print(text, '-> отклонено:', str(e))
Вывод
25 -> 25
300 -> отклонено: возраст вне диапазона: 300

Типичный приём: своя ошибка вырастает поверх встроенной проверки. Разбор строки в число оставлен int, а бизнес-правило про разумный возраст — вашему классу. Вызывающий код теперь может поймать и AgeError (нарушено правило возраста), и ValueError (строка вообще не число) — по отдельности, как две разные ситуации.

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

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

class MyError(Exception):
    pass

try:
    raise MyError('что-то пошло не так')
except MyError as e:
    print(type(e).__name__, '-', str(e))
class Base1(Exception):
    pass

class Child(Base1):
    pass

print(issubclass(Child, Base1), issubclass(Child, Exception))
class Warn(Exception):
    pass

try:
    raise Warn('порог превышен')
except Exception as e:
    print('поймано через Exception:', type(e).__name__)
Проверь себя
0 / 7

1. От какого класса положено наследовать свои исключения?

2. Какое минимальное определение своего исключения рабочее?

3. Что увидит читатель traceback, если своё исключение никто не поймал?

4. Зачем в своём исключении переопределяют __init__?

5. PaymentError и DeliveryError наследованы от ShopError. Сработает ли except ShopError при DeliveryError?

6. Когда стоит заводить собственный класс исключения?

7. Зачем в __init__ своего исключения вызов super().__init__(reason)?

Карточки терминов
Запомнено: 0 / 7
Практика

Создай свой класс EmptyCartError от Exception с телом pass. В функции check_cart(items) выбрасывай EmptyCartError с текстом «корзина пуста», если список items пуст (проверка if not items). Вызови check_cart([]) в try и напечатай «магазин: {текст ошибки}».

practice.py
Вопросы и ответы по уроку

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

Объявить класс-наследник Exception — часто с телом из одной строки pass — и бросить его обычным raise: class MyError(Exception): pass, затем raise MyError('текст'). Ловится оно как встроенное: except MyError as e, сообщение возвращает str(e).

Урок 7: raise — бросаем исключение сами

Почему свои исключения наследуют от Exception, а не от BaseException?

BaseException — корень дерева, в котором живут и сигналы прерывания KeyboardInterrupt и SystemExit. except Exception ловит только ствол Exception, поэтому исключение от BaseException пропущено всеми обычными обработчиками и уронит программу. Ствол Exception — правильное место для своих ошибок.

Урок 9: иерархия исключений

Можно ли хранить данные в объекте исключения?

Да: переопределите __init__, вызовите super().__init__(строка сообщения) и сохраните остальное в атрибуты — e.order_id, e.reason. Вызывающий код работает с полями вместо разбора строк, а сообщение при этом видно в traceback и журналах.

Как организовать несколько своих исключений?

Завести общего предка — class ShopError(Exception) — и наследовать конкретные ошибки от него: PaymentError, DeliveryError. Один обработчик по предку ловит всё семейство, отдельные ветки по детям различают ситуации. Порядок в цепочке: дети раньше родителя.

Проект раздела: надёжный разбор грязных данных

Понравился урок? Сошлитесь на него

«Своё исключение стоит заводить там, где вызывающему коду нужно различить ситуацию и отреагировать выборочно.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.