Свои исключения в 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 читается как заголовок в журнале: тип ошибки — имя вашего класса, далее — сообщение, которое вы передали в скобках. Строка в теле класса — 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__)
1. От какого класса положено наследовать свои исключения?
2. Какое минимальное определение своего исключения рабочее?
3. Что увидит читатель traceback, если своё исключение никто не поймал?
4. Зачем в своём исключении переопределяют __init__?
5. PaymentError и DeliveryError наследованы от ShopError. Сработает ли except ShopError при DeliveryError?
6. Когда стоит заводить собственный класс исключения?
7. Зачем в __init__ своего исключения вызов super().__init__(reason)?
Создай свой класс EmptyCartError от Exception с телом pass. В функции check_cart(items) выбрасывай EmptyCartError с текстом «корзина пуста», если список items пуст (проверка if not items). Вызови check_cart([]) в try и напечатай «магазин: {текст ошибки}».
Как создать своё исключение в 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-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Исключения · Урок 7
raise: бросаем исключение сами
До сих пор ошибки бросал Python. Теперь научимся бросать сами: raise ValueError с понятным текстом, когда данные нарушают правила программы.
Исключения · Урок 9
Иерархия исключений Python: кто чей родитель
ZeroDivisionError, KeyError, FileNotFoundError — не набор случайных имён, а семейное дерево. Разбираемся, кто чей родитель и почему ловля родителя ловит детей.
Исключения · Урок 20
Проект: надёжный разбор файла с грязными данными в Python
Итоговый проект: файл с мусором — пустые строки, слова вместо чисел, пробелы — не роняет отчёт, а попадает в журнал. Три куска: построчный разбор, счётчики с первыми ошибками и функция-парсер со сверкой.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
pytest · Урок 9
Проверка исключений: pytest.raises
Хорошая функция на плохом входе не молчит, а бросает исключение — и тест обязан это проверять. pytest.raises делает это одной строкой внутри with.
pytest проверка исключенияpytest DID NOT RAISE
asyncio · Урок 11
Исключения в конкурентном коде
Одна корутина с ValueError способна уронить весь gather. Флаг return_exceptions=True превращает исключения в значения списка — пачка выживает, а отчёт собирается целиком.
asyncio исключения gatherTask exception was never retrieved
Исключения · Урок 1
Что такое исключение: первая ошибка в программе
Первая ошибка в программе: читаем traceback за десять секунд — тип ошибки, место и сообщение. Разбираем четыре типовых падения на живом коде.
python исключенияисключения python