Рисуем красивые трейсбеки, перехватывая исключения в Python

в 12:28, , рубрики: python, Блог компании Wunder Fund, разработка, Разработка веб-сайтов

Все мы тратим немало времени на отладку, копаясь в логах или читая трейсбеки (traceback, отчёты о трассировке стека). Любое из этих дел может оказаться сложным и длительным. Этот материал посвящён тому, как сделать трассировку стека и работу с исключениями как можно более простыми и эффективными.

Рисуем красивые трейсбеки, перехватывая исключения в Python - 1

На пути к этой цели мы узнаем о том, как реализовывать и использовать собственные перехватчики исключений (exception hook), которые позволяют убрать из трейсбеков весь «информационный шум». Мы поговорим о том, как улучшить читабельность отчётов о трассировке стека, как выводить в них лишь то, что нужно для решения проблем с Python-кодом и с возникающими в процессе его работы исключениями. Кроме того, мы посмотрим на несколько потрясающих Python-библиотек, в которых имеются готовые к использованию, хорошо сделанные перехватчики исключений. Их можно использовать без необходимости написания собственного кода перехватчиков.

Перехватчики исключений

Всякий раз, когда возникает исключение, которое не обрабатывается в блоке try/except, вызывается функция, назначенная sys.excepthook. Эта функция, называемая перехватчиком исключения, применяется для вывода любых полезных сведений о происшествии в стандартный поток вывода. Для этого она пользуется тремя получаемыми ей аргументами — type (класс исключения), value (экземпляр исключения) и traceback (объект трассировки).Разберём минимальный пример, демонстрирующий работу этого механизма:

import sys

def exception_hook(exc_type, exc_value, tb):
    print('Traceback:')
    filename = tb.tb_frame.f_code.co_filename
    name = tb.tb_frame.f_code.co_name
    line_no = tb.tb_lineno
    print(f"File {filename} line {line_no}, in {name}")
    # Класс и экземпляр исключения
    print(f"{exc_type.name}, Message: {exc_value}")
sys.excepthook = exception_hook

Тут мы применяем вышеупомянутые аргументы для того чтобы вывести основные данные трассировки, имеющие отношение к исключению. А именно, объект трассировки (tb) используется для работы со сведениями о трассировке кадров стека. Там содержатся данные, описывающие место возникновения исключения — имя файла (f_code.co_filename), имя функции/модуля (f_code.co_name) и номер строки (tb_lineno).

Кроме того, мы выводим и сведения о самом исключении, пользуясь переменными exc_type и exc_value.

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

def do_stuff():
    # ... сделать что-то такое, что вызывает исключение
    raise ValueError("Some error message")
do_stuff()
Traceback:
File /home/some/path/exception_hooks.py line 22, in <module>
ValueError, Message: Some error message

Здесь имеются лишь некоторые сведения об исключении. Для того чтобы увидеть всю информацию, необходимую для отладки кода, а так же — чтобы получить полное представление о том, где и почему произошло исключение, нам нужно поглубже «зарыться» в объект трассировки:

def exception_hook(exc_type, exc_value, tb):
    local_vars = {}
    while tb:
        filename = tb.tb_frame.f_code.co_filename
        name = tb.tb_frame.f_code.co_name
        line_no = tb.tb_lineno
        print(f"File {filename} line {line_no}, in {name}")
        local_vars = tb.tb_frame.f_locals
        tb = tb.tb_next
    print(f"Local variables in top frame: {local_vars}")
...
File /home/some/path/exception_hooks.py line 41, in <module>
File /home/some/path/exception_hooks.py line 7, in do_stuff
Local variables in top frame: {'some_var': 'data'}

Как видите, объект трассировки (tb), на самом деле, представляет собой связный список произошедших исключений — стектрейс. Этот факт позволяет нам пройтись по данному списку с помощью tb_next и вывести сведения о каждом кадре стека. Более того — можно, воспользовавшись атрибутом tb_frame.f_locals, вывести в консоль локальные переменные. Это способно оказать нам помощь при отладке кода.

Просмотр содержимого объекта трассировки, как мы уже видели, имеет смысл, но получаемые данные плохо организованы, их, по мере роста масштабов задачи, всё сложнее и сложнее воспринимать. Поэтому лучше будет воспользоваться модулем traceback, в котором имеется множество вспомогательных функций, нацеленных на извлечение информации об исключениях.

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

Создание собственных перехватчиков исключений

Когда происходит перехват исключения, вывод данных о нём в stdout — это далеко не всё, что можно с ним сделать. В частности, ещё можно автоматически вывести данные об исключении в файл:

LOG_FILE_PATH = "./some.log"
FILE = open(LOG_FILE_PATH, mode="w")
def exception_hook(exc_type, exc_value, tb):
    FILE.write("*** Exception: ***n")
    traceback.print_exc(file=FILE)
    FILE.write("n*** Traceback: ***n")
    traceback.print_tb(tb, file=FILE)
    
*** Exception: ***
NoneType: None
# 
*** Traceback: ***
  File "/home/some/path/exception_hooks.py", line 82, in <module>
    do_stuff()
  File "/home/some/path/exception_hooks.py", line 7, in do_stuff
    raise ValueError("Some error message")

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

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

import logging
logging.basicConfig(
    level=logging.CRITICAL,
    format='[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s',
    datefmt='%H:%M:%S',
    stream=sys.stdout
)
def exception_hook(exc_type, exc_value, exc_traceback):
    logging.critical("Uncaught exception:", exc_info=(exc_type, exc_value, exc_traceback))
[17:28:33] {/home/some/path/exception_hooks.py:117} CRITICAL - Uncaught exception:
Traceback (most recent call last):
  File "/home/some/path/exception_hooks.py", line 122, in <module>
    do_stuff()
  File "/home/some/path/exception_hooks.py", line 7, in do_stuff
    raise ValueError("Some error message")
ValueError: Some error message

Когда пытаешься улучшить то, что выводится в консоль, первое, что приходит в голову — оформление текста путём цветового выделения самого важного:

# pip install colorama
from colorama import init, Fore
init(autoreset=True)  # Сбросить цвета после каждой операции вывода данных
def exception_hook(exc_type, exc_value, tb):
    local_vars = {}
    while tb:
        filename = tb.tb_frame.f_code.co_filename
        name = tb.tb_frame.f_code.co_name
        line_no = tb.tb_lineno
        # Снабдить строку сведениями о нужном цвете (например - цветом с кодом RED)
        print(f"{Fore.RED}File {filename} line {line_no}, in {name}")
        local_vars = tb.tb_frame.f_locals
        tb = tb.tb_next
    print(f"{Fore.GREEN}Local variables in top frame: {local_vars}")

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

И наконец — мне хотелось бы вас предостеречь. Всякий раз, когда вы решаете установить перехватчик исключений, учитывайте то, что применяемые вами библиотеки могут устанавливать собственные перехватчики. Удостоверьтесь в том, что вы эти перехватчики не переопределили. В подобных случаях можно поступить иначе — воспользоваться конструкцией try/except и, в блоке except, вывести необходимые сведения, например — с помощью sys.exc_info().

Существующие перехватчики, достойные внимания

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

Начну с моего любимого инструмента — с Rich:

# https://rich.readthedocs.io/en/latest/traceback.html
pip install rich
python -m rich.traceback
from rich.traceback import install
install(show_locals=True)
do_stuff()  # Вызывает ValueError
Трейсбек, который выводит Rich
Трейсбек, который выводит Rich

Воспользоваться этим инструментом очень просто — всё сводится к установке соответствующей библиотеки, к её импорту в проект и к вызову функции install, которая подключает перехватчик исключений. Если вам интересно просто посмотреть на то, что выводит Rich, не занимаясь при этом написанием Python-кода, можете прибегнуть к команде python -m rich.traceback.

Ещё один популярный инструмент такого рода — это библиотека better_exceptions. Она тоже очень приятно оформляет выводимые данные, но, в отличие от Rich, для её настройки нужно приложить немного больше усилий:

# https://github.com/Qix-/better-exceptions
pip install better_exceptions
export BETTER_EXCEPTIONS=1
import better_exceptions
better_exceptions.MAX_LENGTH = None
Проверьте, установлена ли переменная TERM в значение xterm, если это не так - настройте следующую переменную,
Смотрите это сообщение о проблеме: https://github.com/Qix-/better-exceptions/issues/8
better_exceptions.SUPPORTS_COLOR = True
better_exceptions.hook()
do_stuff()  # Вызывает ValueError

Помимо установки библиотеки better-exceptions с помощью pip, нам, чтобы её включить, нужно ещё установить переменную окружения BETTER_EXCEPTIONS=1. После этого библиотеку надо настроить с помощью вышеприведённого Python-кода. Самое важное в нём — это вызов функции hook, которая устанавливает перехватчик исключений. Кроме того, в нём мы устанавливаем SUPPORTS_COLOR в значение True. Необходимость выполнения этой настройки зависит от используемого терминала. В частности, она нужна в том случае, если в TERM записано значение, отличающееся от xterm.

Трейсбек, который выводит better-exceptions
Трейсбек, который выводит better-exceptions

Следующим номером нашей программы будет библиотека pretty_errors. Из тех инструментов, о которых я рассказываю, этот, определённо, отличается самой простой настройкой. Для того чтобы им воспользоваться, его достаточно импортировать в проект:

# https://github.com/onelivesleft/PrettyErrors/
pip install pretty_errors
import pretty_errors
если вас устраивают стандартные настройки - можно обойтись без configure
pretty_errors.configure(
    filename_display    = pretty_errors.FILENAME_EXTENDED,
    line_number_first   = True,
    display_link        = True,
    line_color          = pretty_errors.RED + '> ' + pretty_errors.default_config.line_color,
    code_color          = '  ' + pretty_errors.default_config.line_color,
    truncate_code       = True,
    display_locals      = True
)
do_stuff()

В предыдущем фрагменте кода, помимо обязательной конструкции import, показаны настройки библиотеки, без которых можно обойтись. Это — лишь малая часть тех настроек, которые поддерживает библиотека. Полный список конфигурационных опций pretty_errors можно найти здесь.

Трейсбек, который выводит pretty_errors
Трейсбек, который выводит pretty_errors

Наша следующая библиотека выводит трейсбеки в таком стиле, который окажется знакомым любому, кто пользуется Jupyter Notebook. Речь идёт об IPython-модуле ultratb. Он позволяет выводить сообщения об ошибках и трейсбеки, которые хорошо выглядят и легко читаются:

# https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.ultratb.html
pip install ipython
import IPython.core.ultratb
Ещё - ColorTB, FormattedTB, ListTB, SyntaxTB
sys.excepthook = IPython.core.ultratb.VerboseTB(color_scheme='Linux')  # Другие цветовые схемы: NoColor, LightBG, Neutral
do_stuff()
Трейсбек, который выводит ultratb
Трейсбек, который выводит ultratb

И вот — ещё одна библиотека, stackprinter, последняя в моём списке, но далеко не последняя по своим возможностям. Она выдаёт чёткие сведения о проблемах, содержащие всю необходимую отладочную информацию. Для того чтобы ей воспользоваться — достаточно установить перехватчик исключений:

# https://github.com/cknd/stackprinter
pip install stackprinter
import stackprinter
stackprinter.set_excepthook(style='darkbg2')
do_stuff()
Трейсбек, который выводит stackprinter
Трейсбек, который выводит stackprinter

Итоги

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

А вот что я бы точно порекомендовал — так это выбрать одну из этих библиотек и установить её во всех проектах, над которыми вы работаете. Сделать это стоит для совершенствования отладки кода и ради единообразия используемых инструментов. Чем интенсивнее вы используете один из вышеописанных перехватчиков исключений — тем больше вы привыкаете к выдаваемым им данным, и, как следствие, тем больше пользы можете из них извлечь.

Учитывая вышесказанное — советую вам подумать о том, чтобы убрать собственные перехватчики исключений из продакшн-сборок. Дело в том, что особенности оформления выходных данных могут скрыть от нас некоторые сведения об ошибках, которые, в определённых ситуациях, могут оказаться крайне важными. Скажем, в некоторых из вышеприведённых примеров отсутствуют пути к файлам. Это способствует улучшению читабельности вывода при локальной отладке, но может усложнить жизнь программисту при отладке кода, выполняющегося на удалённой системе.

О, а приходите к нам работать? 😏

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде.

Автор:
mr-pickles

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js