Нюансы обработки исключений в Python
Постановка задачи
Обычно при проектировании чистой архитектуры есть часть сервиса, отвечающего за сохранение сущностей (Entity) бизнес-логики (BL) в хранилище (Repository). Хранилищем как вариант может выступать БД. Сохранение осуществляет репозиторий . Есть тесты, покрывающие уже существующий функционал.
ℹ Entity - это такой аналог привычных моделей ORM, только без привязки к БД, технологии, а отражающие суть BL
⚠ Отмечу, что при написании ABC в качестве интерфейса лучше использовать именно наследование от abc.ABC, а не
Реализация репозитория выглядит так
И теперь наши тесты. Я использую pytest для юнит-тестирования.
Запускаем тест
Update исходной задачи
Теперь представим, что в какой-то момент у нас стало несколько Repository и все они рейзят одинаковое исключение. И это одинаковое описание попадает в logger - разобраться где оно возникло крайне сложно.
Решений этой проблемы несколько:
- изменить исходный код метода save_entities, добавив туда зависимость от model_cls
- добавить блок try/except в место, где используется Repository. Перехватив исключение, модифицировать как-то так:
1-й вариант предполагает, что мы изменяем уже оттестированный код, чего мы делать не хотим.
2-й вариант нарушает инкапсуляцию метода (раскрывает внутренную реализацию репозитория) - один из основных принципов ООП. Подробнее про ООП тут. Плюс к этому, втыкать посреди высокоуровневого BL-кода такой блок - значит ухудшить читаемость.
Есть 3-й вариант - написать декоратор, который не будет затрагивать исходную реализацию метода, а красиво добавлять мета-инфу о классе entity, где произошло исключение.
Реализация декоратора
При реализации декоратора, помимо использования functools.wraps для сохранения информации об изначальной функции, важно не потерять ИСХОДНЫЙ ТИП исключения. Вот как это может произойти.
Теперь запуск старый тестов, рассчитанных на обработку TypeError, упадут
Все потому, что мы ПОДМЕНИЛИ исходный тип исключений общим Exception.
Правильный вариант декоратора
Вызов raise ex.__class__() позволяет зарейзить исходный тип исключения (а ведь у нас может быть своя иерархия исключений - тогда этот кейс еще критичнее !!!)
Вызов raise from позволяет сохранить исходный трейсбек исключения, что поможет в отладке и поиске причины.
Итоги
Исключения - мощный механизм "защитного программирования", позволяющий сигнализировать о некорректном состоянии данных/системы и строить поведенческие цепочки на этом основании.
Обработка исключений может строится как на стандартных типах, предоставляемых языком, так и на пользовательских типах исключений. В этом случае при обработке важно не потерять исходное исключение, дабы не нарушить флоу обработки.
@Декоратор - отличный паттерн, который может помочь нам в этом.
🖤 Подписывайтесь на мою телегу. Больше кода 🐍 - меньше багов 🪲!