сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Асинхронность в Python

Исключения и контекстные менеджеры в Python

Зачем это спрашивают

Обработка ошибок - то, что отличает прод-код от ноутбука. Интервьюер ищет рефлексы: ловить конкретное, сохранять причину, логировать с трейсбеком, и физическое отвращение к except: pass.

// «Как организуешь обработку ошибок в пайплайне?» - вопрос, где за минуту видно культуру кода.

Ловить конкретное, убирать в finally

Лови то, что ждёшь: except ValueError, а не голый except - широкая ловля прячет баги, а совсем голый except глотает даже KeyboardInterrupt.

Полная форма try/except/else/finally: else исполняется, когда исключения не было, он сужает try до одной опасной строки; finally - при любом исходе, это место уборки.

// Исключения - для исключительного: поток управления через них в горячем цикле дорог. Хотя сам язык живёт на них: for останавливается по StopIteration.

finally
блок, исполняемый при любом исходе try - уборка ресурсов

Свои исключения и цепочки причин

Доменные ошибки - иерархией от своего корня: class AppError(Exception), от него ветки. Вызывающий ловит семейство одной строкой, а не зоопарк чужих типов.

raise NewError(…) from e сохраняет причину - в трейсбеке видна вся цепочка. Голый raise внутри except перевыбрасывает то же исключение с полным стеком.

// Идиома языка - EAFP (easier to ask forgiveness than permission): попробуй и поймай (try/except KeyError) вместо LBYL-проверок (look before you leap) if key in … - меньше гонок между проверкой и действием, чище код.

raise … from e
явная причинная цепочка исключений в трейсбеке
EAFP / LBYL
easier to ask forgiveness / look before you leap

logging, assert и warnings - по назначению

logging вместо print: уровни, контекст, а logger.exception внутри except пишет ERROR с полным трейсбеком автоматически. Настройка логирования - в точке входа приложения; библиотека берёт только getLogger(__name__).

assert - для инвариантов разработки, не для валидации входа: запуск с python -O вырезает все assert'ы, и «проверка» исчезает молча.

// warnings.warn с DeprecationWarning - канал «работает, но перестанет»: так честно сносят старые API.

logger.exception
ERROR-лог с автоматическим трейсбеком; зовётся из except

Как отвечать: «Как организуешь обработку ошибок в пайплайне, обрабатывающем записи?»

Два уровня. На уровне записи ловлю конкретные ожидаемые исключения: битая запись уходит в logger.exception с id и контекстом - пайплайн живёт дальше, но след остаётся, и по нему видно масштаб проблемы. Неожиданные исключения не глотаю - они летят наверх, где либо ретрай с бэкоффом, либо честное падение с алертом. Свои ошибки строю иерархией от доменного корня, чтобы ловить семейство. И принципиально никаких except: pass - молча съеденная ошибка вернётся кривыми данными через месяц, когда концов уже не найти.

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

На чём валят

  • except: pass - ошибка исчезла, данные побились молча; худший антипаттерн языка.
  • except Exception в цикле: одна битая запись должна логироваться, а не валить всё, но и не глотаться без следа.
  • raise NewError(…) без from e - потерянная причина, отладка вслепую.
  • assert для проверки входа - python -O её вырежет.
  • Ловить исключение слишком далеко от места - из двадцати строк try виновную уже не найти.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 19, остальные разбираются в тренажёре.

  1. #errors_context1 / 5
    В конструкции try / except / else / finally — что и когда выполняется?
    A)else выполняется, если исключения не было; finally — всегда, и при ошибке, и без неё, например для освобождения ресурса
    B)Else выполняется только при возникновении исключения, а finally — наоборот, лишь если исключения в блоке try не случилось
    C)И else, и finally выполняются исключительно тогда, когда внутри блока try было выброшено хотя бы одно исключение
    D)Finally выполняется лишь в случае, когда исключение не было перехвачено ни одним из блоков except, стоящих выше по коду
    показать ответ и разбор
    +A)else выполняется, если исключения не было; finally — всегда, и при ошибке, и без неё, например для освобождения ресурса

    // разбор: try — защищаемый код; except — обработка конкретных ошибок; else — выполняется только если try прошёл без исключений (отделяет рискованную часть от продолжения); finally — выполняется всегда, при любом исходе, в том числе при return внутри try, и служит для гарантированного освобождения ресурсов. Это и делает finally надёжным местом для очистки.

  2. #errors_context2 / 5
    Чем плохо писать голый except: без указания типа исключения?
    A)Ничем не плохо: это рекомендуемый и самый безопасный способ обработки, ловящий ровно нужные ошибки максимально точечно
    B)Он ловит вообще всё подряд, включая KeyboardInterrupt и SystemExit, и прячет опечатки и баги — сужай до конкретных типов
    C)Голый except не рекомендуется в Python и приводит к синтаксической ошибке ещё на этапе разбора кода
    D)Он замедляет программу в несколько раз, потому что заставляет интерпретатор проверять каждый её оператор на наличие ошибки
    показать ответ и разбор
    +B)Он ловит вообще всё подряд, включая KeyboardInterrupt и SystemExit, и прячет опечатки и баги — сужай до конкретных типов

    // разбор: Голый except (и почти столь же грубый except Exception) перехватывает всё подряд, включая системные KeyboardInterrupt и SystemExit, а также незамеченные баги вроде опечаток в именах, — программа молча продолжает работу в неверном состоянии. Ловят конкретные ожидаемые типы (except ValueError), а неожиданное пусть падает громко, с трейсбеком.

  3. #errors_context3 / 5
    Что означает питонический принцип EAFP по сравнению с LBYL?
    A)EAFP требует заранее проверить все условия длинной цепочкой if, прежде чем выполнять потенциально опасное действие
    B)EAFP и LBYL — это просто два названия одного и того же подхода, разницы между ними в реальном коде совершенно никакой нет
    C)EAFP предписывает не пользоваться исключениями, заменяя их возвращаемыми кодами ошибок из функций
    D)EAFP («проще просить прощения») — делать действие в try/except; LBYL — проверять условия заранее через if. В Python EAFP часто чище
    показать ответ и разбор
    +D)EAFP («проще просить прощения») — делать действие в try/except; LBYL — проверять условия заранее через if. В Python EAFP часто чище

    // разбор: LBYL (look before you leap) проверяет условия заранее: if key in d: use(d[key]). EAFP (easier to ask forgiveness) просто делает и ловит исключение: try: use(d[key]) except KeyError: .... EAFP часто короче и свободен от гонки (между проверкой и действием состояние могло измениться), поэтому в Python его нередко предпочитают.

  4. #errors_context4 / 5
    Зачем заводить свои классы исключений вместо raise Exception('...')?
    A)Свои исключения обязательны: без них интерпретатор Python запрещает использовать конструкцию raise где-либо в коде программы
    B)Ради скорости: пользовательские классы исключений обрабатываются интерпретатором быстрее встроенных типов
    C)Чтобы вызывающий ловил их точечно (except MyError) и отличал ожидаемые доменные ошибки от прочих — тип несёт смысл
    D)Свои классы исключений автоматически логируют себя в файл и шлют уведомление разработчику вообще без какой-либо настройки
    показать ответ и разбор
    +C)Чтобы вызывающий ловил их точечно (except MyError) и отличал ожидаемые доменные ошибки от прочих — тип несёт смысл

    // разбор: Свой тип исключения (class OrderNotFound(Exception)) позволяет вызывающему поймать именно его — except OrderNotFound — не хватая заодно все прочие ошибки, и несёт доменный смысл. raise Exception слишком широк: отличить такую ошибку можно лишь по тексту, что хрупко. Иерархия своих исключений делает обработку точной и читаемой, а обработчики — устойчивыми.

  5. #errors_context5 / 5
    Блок except перехватывает ошибку, пишет её в лог и делает pass. Чем это опасно?
    A)Ничем: перехватить ошибку, залогировать её и продолжить выполнение — правильная и безопасная практика в проде
    B)Проглатывание ошибки: программа идёт дальше в неверном состоянии, баг замаскирован. Обрабатывай конкретное или пробрасывай (raise)
    C)Опасность лишь в том, что запись в лог заметно замедляет программу, поэтому логирование ошибки из блока лучше вообще убрать
    D)Pass в блоке except вызывает бесконечный цикл повторной обработки той же ошибки, пока у процесса не закончится вся память
    показать ответ и разбор
    +B)Проглатывание ошибки: программа идёт дальше в неверном состоянии, баг замаскирован. Обрабатывай конкретное или пробрасывай (raise)

    // разбор: Перехват широкого исключения с последующим pass или continue проглатывает проблему: функция «как бы отработала», хотя данные или состояние испорчены, а настоящая причина всплывёт позже и далеко от источника. Ловят конкретный ожидаемый тип и осмысленно его обрабатывают; неизвестное пробрасывают дальше (re-raise) или дают упасть, чтобы увидеть трейсбек.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.