Статьи

Как отлаживать ошибки Python

Эта статья была первоначально опубликована на Sentry . Спасибо за поддержку партнеров, которые делают возможным использование SitePoint.

Одна из более мощных функций Sentry поставляется с такими языками, как Python. В Python, PHP и JVM мы можем более глубоко исследовать среду выполнения и предоставить вам дополнительные данные о каждом кадре в вашем стеке вызовов. На высоком уровне это позволяет вам знать такие вещи, как вызов аргументов для ваших функций, позволяя вам легче воспроизводить и понимать ошибку. Давайте углубимся в то, как это выглядит и как это работает под капотом.

Начнем с того, как обычно может выглядеть ошибка Python в вашем терминале или в стандартной системе регистрации:

TypeError: expected string or buffer File "sentry/stacktraces.py", line 309, in process_single_stacktrace processable_frame, processing_task) File "sentry/lang/native/plugin.py", line 196, in process_frame in_app = (in_app and not self.sym.is_internal_function(raw_frame.get('function'))) File "sentry/lang/native/symbolizer.py", line 278, in is_internal_function return _internal_function_re.search(function) is not None 

Хотя это дает нам представление о типе и местонахождении ошибки, к сожалению, это не помогает нам понять, что действительно вызывает ее. Возможно, что он передает целое число или тип NoneType, но на самом деле это может быть любое количество вещей. Гадание на этом только поможет нам, и нам действительно нужно знать, что на самом деле является function .

Простой и часто очень доступный вариант сделать это — добавить запись в журнал. Есть несколько разных точек входа, в которые мы могли бы поместить ведение журнала, что облегчает реализацию. Это также позволяет нам получить конкретный ответ, который мы хотим. Например, мы хотим выяснить type function :

 import logging # ... logging.debug("function is of type %s", type(function)) 

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

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

Отладчик Python

Отладчик Python (PDB) — это инструмент, который позволяет вам проходить через ваш стек вызовов с помощью точек останова. Сам инструмент вдохновлен отладчиком GNU (GDB) и, несмотря на свою мощь, он часто может быть ошеломляющим, если вы с ним не знакомы. Это, безусловно, опыт, который станет легче с повторением, и мы только углубимся в несколько концепций высокого уровня с ним для нашего примера.

Итак, первое, что мы хотели бы сделать — это добавить в наш код точку останова. В нашем примере выше, мы могли бы на самом деле инструмент symbolizer.py сами. Это не всегда так, так как иногда исключение происходит в стороннем коде. Независимо от того, где вы его используете, вы все равно сможете прыгать вверх и вниз по стеку. Давайте начнем с изменения этого кода:

 def is_internal_function(self, function): # add a breakpoint for PDB try: return _internal_function_re.search(function) is not None except Exception: import pdb; pdb.set_trace() raise 

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

Как только мы достигнем этой точки останова (что set_trace() ), мы set_trace() в специальную среду, похожую на оболочку:

 # ... (Pdb) 

Это консоль PDB, и она работает аналогично оболочке Python. Помимо возможности выполнять большую часть кода Python, мы также выполняем в определенном контексте в нашем стеке вызовов. Это место является точкой входа. Скорее, это где вы вызвали set_trace() . В приведенном выше примере мы находимся именно там, где должны быть, поэтому мы можем легко получить тип function :

 (Pdb) type(function) <type 'NoneType'> 

Конечно, мы также можем просто получить все переменные локальной области видимости, используя одну из встроенных функций Python:

 (Pdb) locals() {..., 'function': None, ...} 

В некоторых случаях нам, возможно, придется перемещаться up и down по стеку, чтобы добраться до кадра, в котором выполняется функция. Например, если наш инструментарий set_trace() опустил нас выше в стеке, возможно, в верхнем кадре, мы использовали бы down чтобы перейти во внутренние кадры, пока не достигнем места, в котором была нужная информация:

 (Pdb) down -> in_app = (in_app and not self.sym.is_internal_function(raw_frame.get('function'))) (Pdb) down -> return _internal_function_re.search(function) is not None (Pdb) type(function) <type 'NoneType'> 

Итак, мы определили проблему: function является NoneType . Хотя это на самом деле не говорит нам, почему это так, по крайней мере, это дает нам ценную информацию для ускорения наших тестов.

Отладка в производстве

Таким образом, PDB отлично работает в разработке, но как насчет производства? Давайте посмотрим немного глубже на то, что Python дает нам, чтобы ответить на этот вопрос.

Самое замечательное в среде выполнения CPython — это стандартная среда выполнения, которую использует большинство людей, — она ​​обеспечивает легкий доступ к текущему стеку вызовов. Хотя некоторые другие среды выполнения (например, PyPy) предоставляют аналогичную информацию, это не гарантируется. Когда вы sys.exc_info() исключение, стек sys.exc_info() через sys.exc_info() . Давайте посмотрим, что это дает нам для типичного исключения:

 >>> try: ... 1 / 0 ... except: ... import sys; sys.exc_info() ... (<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x105da1a28>) 

Мы собираемся избегать углубления в это, но у нас есть кортеж из трех частей информации: класс исключения, фактический экземпляр исключения и объект трассировки. Здесь нам важен объект трассировки. Следует отметить, что вы также можете получить эту информацию за пределами исключений, используя модуль трассировки . Есть некоторая документация по использованию этих структур, но давайте просто погрузимся в себя, чтобы попытаться понять их. В объекте traceback у нас есть куча информации, доступной для нас, хотя для доступа к ней потребуется немного работы — и волшебства:

 >>> exc_type, exc_value, tb = exc_info >>> tb.tb_frame <frame object at 0x105dc0e10> 

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

 def foo(bar=None): foo = "bar" 1 / 0 

Давайте сгенерируем исключение с этим кодом:

 try: foo() except: exc_type, exc_value, tb = sys.exc_info() 

И, наконец, давайте перейдем к f_locals через f_locals для объекта <frame> :

 >>> from pprint import pprint >>> pprint(tb.tb_frame.f_locals) {'__builtins__': <module '__builtin__' (built-in)>, '__doc__': None, '__name__': '__main__', '__package__': None, 'exc_info': (<type 'exceptions.ZeroDivisionError'>, ZeroDivisionError('integer division or modulo by zero',), <traceback object at 0x105cd4fc8>), 'foo': <function foo at 0x105cf50c8>, 'history': '/Users/dcramer/.pythonhist', 'os': <module 'os' from 'lib/python2.7/os.py'>, 'pprint': <function pprint at 0x105cf52a8>, 'print_function': _Feature((2, 6, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0), 65536), 'readline': <module 'readline' from 'lib/python2.7/lib-dynload/readline.so'>, 'stack': [], 'sys': <module 'sys' (built-in)>, 'tb': <traceback object at 0x105da1a28>, 'tb_next': None, 'write_history': <function write_history at 0x105cf2c80>} 

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

 >>> inner_frame = tb.tb_next.tb_frame >>> pprint(inner_frame.f_locals) {'bar': None, 'foo': 'bar'} 

Вы можете быстро понять, как это может быть полезно, когда мы вернемся к нашей первоначальной TypeError . В этом случае, с помощью вышеуказанного самоанализа, мы обнаруживаем, что function , которая, как ожидается, будет строкой, на самом деле установлена ​​в NoneType . Мы знаем это, потому что Sentry уловил эту ошибку для нас и автоматически извлекает локальные стеки для каждого кадра:

исключение

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

Если вам интересна полная реализация, которая также использует другие различные компоненты из структуры трассировки Python, вы всегда можете взглянуть на исходный код нашего Python SDK на GitHub . В PHP и JVM подход немного отличается из-за времени выполнения, и, если вам интересно, вы также найдете эти репозитории в GentHub от Sentry . Если вы используете Sentry, мы, как правило, автоматически подбираем для вас инструменты, хотя JVM требует небольшой настройки (скоро появятся документы).