Лекция 8
Обработка ошибок и логирование
План лекции
| Секция | Тема | Чему научимся |
|---|---|---|
| A | Контекст и мотивация | Понимать, зачем это нужно |
| B | Основы исключений | Читать ошибки, ловить через try/except |
| C | Углубление try/except | else, finally, типичные ловушки |
| D | raise | Выбрасывать свои исключения по бизнес-правилам |
| E | Контекстные менеджеры (with) | Безопасно работать с ресурсами |
| F | Модуль logging | Писать нормальные логи вместо print() |
| G | Финальная практика | Собрать всё вместе на реальной задаче |
Концепция
| Уровень | Инструмент | Что защищает |
|---|---|---|
| 1 | if / raise | Входные аргументы. Блокируем мусор сразу |
| 2 | try / except | Внешние ресурсы, сеть, I/O, парсинг |
| 3 | with / contextmanager | Ресурсы: файлы, БД, локи |
| 4 | logging | Мониторинг и аудит в продакшне |
Принцип: чем раньше поймана ошибка, тем дешевле её исправить. В реальной системе все уровни работают вместе.
Контекст
Пятница, 18:32, день зарплат. Сервис обрабатывает переводы через Humo. Партнёр обновил API: переименовал один атрибут в JSON-ответе.
Контекст
KeyError до outageresponse["currency"]. Ключа больше нет: KeyError.SLA банка 99.99% uptime = максимум 52 минуты простоя в год. Один необработанный KeyError сжигает половину годового лимита за вечер.
Концепция
Это подход, при котором код пишут с предположением «всё может пойти не так»: сеть упадёт, БД лагает, партнёрский API вернёт мусор, кто-то напишет опечатку.
Не доверяем входным данным.
Не даём упавшему ресурсу убить процесс.
Знаем, что упало и когда.
Связь с лекцией 7
На прошлой лекции мы писали полиморфные шлюзы: UzCardGateway, HumoGateway. Через Duck Typing вызываем gateway.process_settlement(). Одна опечатка ломает прод.
UzCard прошёл, Humo упал. Часть клиентов получила зарплату, часть нет.
Основы исключений
Исключение: сигнал «что-то пошло не так», который Python отправляет, когда не может выполнить операцию. Без обработки программа падает.
Python создал объект KeyError, никто его не обработал, программа упала с Traceback.
Основы исключений
Traceback читается снизу вверх. Нижняя строка: суть проблемы. Строки выше: путь, по которому пришли.
Основы исключений
| Шаг | Что делаем |
|---|---|
| 1 | Читаем последнюю строку: KeyError: 'rate', ключ rate отсутствует |
| 2 | Смотрим строку выше: extract_rate, строка 2, там падение |
| 3 | Идём в код, проверяем строку 2: api_response["data"]["rate"] |
| 4 | Проверяем содержимое api_response: там exchange_rate, а мы ищем rate |
| 5 | Готово: партнёр переименовал ключ |
Основы исключений
try/exceptЧтобы поймать исключение и не дать программе упасть.
try оборачивает опасный код, except ловит конкретный тип ошибки. Программа не падает.
Основы исключений · Типичные ошибки
KeyError и ValueErrorОсновы исключений · Типичные ошибки
TypeError и AttributeErrorTypeError: "1000000" это строка (sequence), её нельзя умножить на float. AttributeError: опечатка в имени метода или метод не реализован.
Основы исключений
Исключения в Python это обычные классы. KeyError наследует LookupError, тот наследует Exception. Каждый except SomeError работает как isinstance(): ловит сам класс и любого его потомка.
Основы исключений
Не перехватывайте BaseException: туда входят SystemExit и KeyboardInterrupt. Иначе нельзя будет даже корректно остановить программу.
Основы исключений
Основы исключений
Исключение это обычный объект. Полезно знать, что у него внутри, чтобы писать осмысленные логи.
type(exc).__name__ идёт в систему мониторинга: по нему фильтруют инциденты.
Вопрос аудитории
[KeyError] поймали: 'нет ключа'[Exception] поймали: 'нет ключа'Подумайте 30 секунд. Ответ на следующем слайде.
Ответ
[Exception] поймалиPython проверяет блоки except сверху вниз и останавливается на первом совпадении. KeyError это потомок Exception, поэтому первый блок ловит ошибку, а второй уже не выполнится.
Правило: от конкретного к общему. Специфичные except сверху, общие снизу.
Углубление try/except
tryЧем меньше кода в try, тем точнее вы понимаете, где упало. Заворачивайте только опасные операции.
Углубление try/except
except: разные ошибки, разные реакцииОдин except Exception на всё это одна кнопка для всех ситуаций. Гранулярные except позволяют реагировать по-разному.
| Тип ошибки | Реакция |
|---|---|
Сеть упала (Timeout) | Ретрай через несколько секунд |
Невалидные данные (ValueError) | Отклонить с кодом 422 |
| Превышен лимит | Уведомить комплаенс |
| Битый JSON от партнёра | Залогировать, вернуть PARTNER_ERROR |
В Python нет страховочной сетки компилятора, который заставит обработать исключения, как в Java. За это отвечаете вы.
Углубление try/except
Вопрос аудитории
process() для фродовой транзакции?BLOCKED: антифрод сработал правильноAPPROVED: антифрод не отработал, программа не упалаNameErrorОтвет
APPROVED, антифрод не отработалscor это опечатка. Python бросает NameError: name 'scor' is not defined.except Exception ловит всё, включая NameError.APPROVED. Мошенник провёл транзакцию.Это самая опасная конструкция в коде. Широкий except Exception без логирования прячет любые баги, включая опечатки в коде.
Правило: перехватывайте только то, что вы ожидаете и умеете обработать.
Углубление try/except
else: выполняется только при успехеЧёткое разделение: «опасная операция», «обработка ошибки» и «код при успехе» лежат в разных блоках.
Если положить «код при успехе» внутрь try, его ошибки тоже поймает except и обработает их как ошибки опасной операции. Это путаница в логах.
Углубление try/except
else на практике: COMMIT только при успехеtry делает запись, except ловит ошибки, else подтверждает транзакцию, finally закрывает соединение в любом случае.
Углубление try/except
else на практике: запуск и выводВ успехе срабатывают try → else → finally. В ошибке: try → except → finally. Блок finally отрабатывает оба раза.
Углубление try/except
finally: выполняется всегдаДля гарантированного освобождения ресурсов: закрыть файл, снять блокировку, закрыть соединение с БД. finally срабатывает даже когда внутри try был return.
Лок снимается всегда. Без finally блокировка могла бы остаться навсегда.
Вопрос аудитории
[EXCEPT], потом [FINALLY], и спокойно завершится[EXCEPT], [FINALLY], потом упадёт с UnboundLocalError[EXCEPT], до finally не дойдётОтвет
UnboundLocalError поверх оригинальной ошибкиtry выбросилось ConnectionRefusedError.db_conn = ... не выполнилась, переменной не существует.except отработал. finally вызвал db_conn.close().UnboundLocalError.Исправление: инициализируйте ресурс None до try.
raise
raise переводит бизнес-правила на язык исключений. То, что синтаксически корректно, может быть бизнес-ошибкой: amount = -50000, перевод на тот же счёт, превышение лимита.
Контекстные менеджеры
Каждый ресурс (файл, соединение, лок) нужно закрыть после работы. Если между открытием и закрытием произойдёт исключение, строка f.close() просто не выполнится.
Это утечка ресурсов: файл занят, память не освобождена. На сервере с тысячами запросов такие утечки накапливаются и валят процесс.
Контекстные менеджеры
withwith говорит Python: «открой это, дай мне поработать, закрой это сам, даже если случится ошибка». То же самое короче и надёжнее, чем try/finally.
Класс с методами __enter__ и __exit__ работает с with. __exit__ вызывается всегда, даже если внутри блока было исключение. В этом и есть гарантия освобождения ресурса.
Контекстные менеджеры
with работает внутриОбъект, переданный в with, обязан иметь два метода: __enter__ (вход в блок) и __exit__ (выход). Python вызывает их за вас.
Главная ценность with: __exit__() вызывается всегда. Даже если внутри блока было исключение, ресурс будет освобождён.
Контекстные менеджеры
Один и тот же синтаксис with работает для файлов, баз данных, блокировок, сокетов, временных директорий.
Контекстные менеджеры
При работе с базой данных with делает сразу две вещи: ведёт транзакцию (commit на успехе, rollback на ошибке) и закрывает соединение в конце.
В фоновой задаче Celery такая же конструкция гарантирует, что после выполнения задачи в БД не останется зависших транзакций и незакрытых соединений между задачами воркера.
Контекстные менеджеры
with и try/except вместеwith отвечает за ресурсы, try/except отвечает за ошибки. Часто их используют вместе.
Файл закрывается автоматически, даже когда исключение поймано блоком try/except снаружи.
Контекстные менеджеры
DBTransaction: автоматический COMMIT или ROLLBACKКонтекстные менеджеры
@contextmanager: менеджер через генераторНе всегда нужен целый класс. Для простых случаев есть декоратор @contextmanager из contextlib: код до yield это __enter__, после yield это __exit__.
Порядок выхода обратен порядку входа. Внутренний лок снимается первым.
Контекстные менеджеры
with и с withБез with | С with | |
|---|---|---|
| Закрытие ресурса | Вручную | Автоматически |
| При ошибке | Ресурс остаётся открытым | Всё равно закрывается |
| Читаемость | Больше кода | Чище и короче |
| Утечки памяти | Возможны | Исключены |
Новый раздел
loggingЛоги: то, что остаётся после сбоя
Модуль logging
Логирование: запись событий программы во время её работы. Что произошло, когда, на каком сервере, в какой функции.
Аналогия: бортовой самописец самолёта. После любого инцидента инженер открывает запись и видит, что происходило в системе перед сбоем.
Модуль logging
print() в продакшне не годится| Проблема | Почему важно |
|---|---|
| Нет уровня важности | Не понимаем, это DEBUG или CRITICAL |
| Нет временной метки | Когда это случилось? |
| Нет файла и строки | Откуда это в коде? |
| Нельзя фильтровать | Не агрегируется в ELK Stack |
| Нельзя ротировать | Файл растёт бесконечно |
| Уровень | Когда использовать |
|---|---|
DEBUG | Детальный трейс для отладки. В продакшне выключен |
INFO | Стандартные события: транзакция создана, запрос принят |
WARNING | Нештатно, но система работает: retry #2, медленный запрос |
ERROR | Одна операция упала, система продолжает работу |
CRITICAL | Серьёзно: база недоступна, под угрозой весь сервис |
Модуль logging
Три шага: получить логгер по имени, задать уровень, добавить обработчик с форматом.
В продакшне дополнительно добавляют RotatingFileHandler: пишет в файл и ротирует его по размеру, чтобы лог не съел диск.
Модуль logging
Уровень выбирается под событие. В продакшне обычно фильтруют от INFO и выше: DEBUG слишком шумный.
Модуль logging
exc_info=True: добавляет traceback в логБез exc_info=True лог покажет только сообщение об ошибке. С exc_info=True добавит полный traceback. Это критично для дебага.
| Уровень | exc_info=True? |
|---|---|
logger.error() | Всегда |
logger.critical() | Всегда |
logger.warning() | Если предупреждение связано с исключением |
logger.info() | Не нужно |
logger.debug() | Не нужно |
Модуль logging
except + обязательное логированиеИногда нельзя предугадать все ошибки, тогда используют except Exception. Но это не оправдание молчать: ловишь всё, обязан записать всё.
| Часть сообщения | Что даёт |
|---|---|
"Неожиданная ошибка антифрода" | Контекст: где именно упало |
{type(e).__name__} | Имя класса ошибки, для фильтрации в Sentry |
{e} | Текст ошибки, для понимания что случилось |
exc_info=True | Полный traceback, для понимания где в коде |
Это называется graceful degradation: сервис не падает, но ошибка зафиксирована.
Модуль logging
Без лога ошибка тихо проглочена. Сервис продолжает «работать», бизнес-логика даёт неверный ответ, данные портятся. Это сокрытие улик, а не защита.
Итоги
| Концепция | Ключевой вывод |
|---|---|
| Чтение Traceback | Снизу вверх: тип, место, путь |
| Иерархия исключений | Это ООП: конкретные except сверху, общие снизу |
try / except / else / finally | В try минимум кода; else при успехе; finally всегда |
Blanket except | except Exception без логирования = тихая смерть |
UnboundLocalError | Инициализируй ресурсы None до try |
raise | Переводим бизнес-правила на язык исключений |
with / contextmanager | Гарантированное освобождение ресурсов |
logging | 5 уровней, exc_info=True, понятный формат сообщений |
Финальная задача
Дано: список платежей. Часть с битыми полями, часть нарушает бизнес-правила, среди получателей есть санкционный. Задача: процессор не падает на любом входе, считает ошибки по трём категориям.
Финальная задача
Три класса ошибок по категориям. Один валидатор: либо OK, либо raise нужного типа.
Финальная задача
Один try/except с тремя ветками. Каждый тип ошибки идёт в свой счётчик и в свой уровень лога. Программа не падает ни на чём.
Материалы
15 заданий и финальная задача «Платёжный процессор». Открывается в Jupyter, VS Code или Google Colab.