Отладка Python с помощью Pdb

Отладка Python с помощью Pdb

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

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

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

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

Это отлично подходит для отслеживания трудно обнаруживаемых ошибок и позволяет быстрее и надежнее исправлять неисправный код. Иногда, шагая по коду в pdb и видя, как изменяются значения, можно по-настоящему открыть глаза и привести к моментам «ага», а также к случайной «ладони».

pdb является частью стандартной библиотеки Python, поэтому он всегда там и доступен для использования. Это может спасти жизнь, если вам нужно отлаживать код в среде, где у вас нет доступа к отладчику GUI, с которым вы знакомы.

Пример кода в этом руководстве использует Python 3.6. Вы можете найти исходный код этих примеров наGitHub.

В конце этого руководства есть краткий справочник поEssential pdb Commands.

Существует также печатный справочник команд pdb, который вы можете использовать в качестве шпаргалки при отладке:

Free Bonus:Click here to get a printable "pdb Command Reference" (PDF), который вы можете держать на столе и использовать во время отладки.

Начало работы: печать значения переменной

В этом первом примере мы рассмотрим использование pdb в его простейшей форме: проверка значения переменной.

Вставьте следующий код в место, где вы хотите взломать отладчик:

import pdb; pdb.set_trace()

Когда строка выше выполнена, Python останавливается и ждет, пока вы не скажете ему, что делать дальше. Вы увидите сообщение(Pdb). Это означает, что вы теперь приостановлены в интерактивном отладчике и можете ввести команду.

Начиная с Python 3.7there’s another way to enter the debugger. PEP 553 описывает встроенную функциюbreakpoint(), которая упрощает и согласовывает вход в отладчик:

breakpoint()

По умолчаниюbreakpoint() импортируетpdb и вызоветpdb.set_trace(), как показано выше. Однако использованиеbreakpoint() более гибкое и позволяет управлять поведением отладки через его API и использовать переменную средыPYTHONBREAKPOINT. Например, установкаPYTHONBREAKPOINT=0 в вашей среде полностью отключитbreakpoint(), тем самым отключив отладку. Если вы используете Python 3.7 или новее, я рекомендую вам использоватьbreakpoint() вместоpdb.set_trace().

Вы также можете взломать отладчик, не изменяя источник и используяpdb.set_trace() илиbreakpoint(), запустив Python непосредственно из командной строки и передав параметр-m pdb. Если ваше приложение принимает аргументы командной строки, передайте их, как обычно, после имени файла. Например:

$ python3 -m pdb app.py arg1 arg2

Доступно много команд pdb. В конце этого руководства есть списокEssential pdb Commands. А пока давайте воспользуемся командойp для вывода значения переменной. Введитеp variable_name в ответ на приглашение(Pdb), чтобы распечатать его значение.

Давайте посмотрим на пример. Вот источникexample1.py:

#!/usr/bin/env python3

filename = __file__
import pdb; pdb.set_trace()
print(f'path = {filename}')

Если вы запустите это из своей оболочки, вы должны получить следующий вывод:

$ ./example1.py
> /code/example1.py(5)()
-> print(f'path = {filename}')
(Pdb)

Если у вас возникли проблемы с запуском примеров или собственного кода из командной строки, прочтитеHow Do I Make My Own Command-Line Commands Using Python?. Если вы работаете в Windows, проверьтеPython Windows FAQ.

Теперь введитеp filename. Тебе следует увидеть:

(Pdb) p filename
'./example1.py'
(Pdb)

Поскольку вы находитесь в оболочке и используете CLI (интерфейс командной строки), обратите внимание на символы и форматирование. Они дадут вам контекст, который вам нужен:

  • > начинает первую строку и сообщает вам, в каком исходном файле вы находитесь. После имени файла в скобках указывается номер текущей строки. Далее идет название функции. В этом примере, поскольку мы не остановились внутри функции и на уровне модуля, мы видим<module>().

  • -> начинает вторую строку и является текущей строкой исходного кода, в которой Python приостановлен. Эта строка еще не выполнена. В этом примере это строка5 вexample1.py из строки> выше.

  • (Pdb) - это приглашение PDB. Он ждет команды.

Используйте командуq, чтобы завершить отладку и выйти.

Печать выражений

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

В этом примере вызывается функцияget_path(). Чтобы проверить, что происходит в этой функции, я вставил вызовpdb.set_trace(), чтобы приостановить выполнение непосредственно перед его возвратом:

#!/usr/bin/env python3

import os


def get_path(filename):
    """Return file's path or empty string if no path."""
    head, tail = os.path.split(filename)
    import pdb; pdb.set_trace()
    return head


filename = __file__
print(f'path = {get_path(filename)}')

Если вы запустите это из своей оболочки, вы должны получить вывод:

$ ./example2.py
> /code/example2.py(10)get_path()
-> return head
(Pdb)

Где мы?

  • >: мы находимся в исходном файлеexample2.py в строке10 в функцииget_path(). Это система отсчета, которую командаp будет использовать для разрешения имен переменных, т.е. текущая область или контекст.

  • ->: выполнение приостановлено наreturn head. Эта строка еще не выполнена. Это строка10 вexample2.py в функцииget_path() из строки> выше.

Давайте напишем некоторые выражения, чтобы взглянуть на текущее состояние приложения. Сначала я использую командуll (longlist), чтобы перечислить источник функции:

(Pdb) ll
  6     def get_path(filename):
  7         """Return file's path or empty string if no path."""
  8         head, tail = os.path.split(filename)
  9         import pdb; pdb.set_trace()
 10  ->     return head
(Pdb) p filename
'./example2.py'
(Pdb) p head, tail
('.', 'example2.py')
(Pdb) p 'filename: ' + filename
'filename: ./example2.py'
(Pdb) p get_path

(Pdb) p getattr(get_path, '__doc__')
"Return file's path or empty string if no path."
(Pdb) p [os.path.split(p)[1] for p in os.path.sys.path]
['pdb-basics', 'python36.zip', 'python3.6', 'lib-dynload', 'site-packages']
(Pdb)

Вы можете передать любое допустимое выражение Pythonp для оценки.

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

Вы также можете использовать командуpp (pretty-print) для выражения pretty-print. Это полезно, если вы хотите напечатать переменную или выражение с большим количеством вывода, например, списки и словари. Симпатичная печать сохраняет объекты на одной строке, если это возможно, или разбивает их на несколько строк, если они не помещаются в пределах допустимой ширины.

Пошаговое выполнение кода

Есть две команды, которые вы можете использовать для пошагового выполнения кода при отладке:

команда Описание

n (далее)

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

s (шаг)

Выполнить текущую строку и остановиться при первой возможности (либо в вызываемой функции, либо в текущей функции).

Есть третья команда с именемunt (до). Это связано сn (следующий). Мы рассмотрим это позже в этом руководстве в разделеContinuing Execution.

Разница междуn (следующий) иs (шаг) заключается в том, где pdb останавливается.

Используйтеn (next), чтобы продолжить выполнение до следующей строки и оставаться в пределах текущей функции, т.е. не останавливаться в чужой функции, если она вызывается. Думайте о следующем, как о «оставаться на месте» или «перешагнуть».

Используйтеs (step) для выполнения текущей строки и остановки во внешней функции, если она вызывается. Думайте о шаге как о «шаге в». Если выполнение остановлено в другой функции,s напечатает--Call--.

Иn, иs остановят выполнение, когда будет достигнут конец текущей функции, и напечатают--Return-- вместе с возвращаемым значением в конце следующей строки после->.

Давайте рассмотрим пример с использованием обеих команд. Вот источникexample3.py:

#!/usr/bin/env python3

import os


def get_path(filename):
    """Return file's path or empty string if no path."""
    head, tail = os.path.split(filename)
    return head


filename = __file__
import pdb; pdb.set_trace()
filename_path = get_path(filename)
print(f'path = {filename_path}')

Если вы запустите это из своей оболочки и введитеn, вы должны получить результат:

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) n
> /code/example3.py(15)()
-> print(f'path = {filename_path}')
(Pdb)

С помощьюn (next) мы остановились на строке15, следующей строке. Мы «оставались локальными» в<module>() и «перешагнули» вызовget_path(). Это функция<module>(), поскольку в настоящее время мы находимся на уровне модуля и не приостановлены внутри другой функции.

Давайте попробуемs:

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb)

С помощьюs (шаг) мы остановились на строке6 в функцииget_path(), поскольку она вызывалась в строке14. Обратите внимание на строку--Call-- после командыs.

Удобно, pdb запоминает вашу последнюю команду. Если вы выполняете много кода, просто нажмите[.kbd .key-enter]#Enter #, чтобы повторить последнюю команду.

Ниже приведен пример использованияs иn для пошагового выполнения кода. Сначала я ввожуs, потому что хочу «войти» в функциюget_path() и остановиться. Затем я ввожуn один раз, чтобы «оставаться на месте» или «перешагивать» любые другие вызовы функций, и просто нажимаю[.kbd .key-enter]#Enter #, чтобы повторять командуn, пока не дойду до последней исходной строки.

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb) n
> /code/example3.py(8)get_path()
-> head, tail = os.path.split(filename)
(Pdb)
> /code/example3.py(9)get_path()
-> return head
(Pdb)
--Return--
> /code/example3.py(9)get_path()->'.'
-> return head
(Pdb)
> /code/example3.py(15)()
-> print(f'path = {filename_path}')
(Pdb)
path = .
--Return--
> /code/example3.py(15)()->None
-> print(f'path = {filename_path}')
(Pdb)

Обратите внимание на строки--Call-- и--Return--. Это pdb, сообщающий вам, почему выполнение было остановлено. n (следующий) иs (шаг) остановятся перед возвратом функции. Вот почему вы видите строки--Return-- выше.

Также обратите внимание на->'.' в конце строки после первого--Return-- выше:

--Return--
> /code/example3.py(9)get_path()->'.'
-> return head
(Pdb)

Когда pdb останавливается в конце функции перед возвратом, он также печатает возвращаемое значение для вас. В этом примере это'.'.

Листинг исходного кода

Не забудьте командуll (длинный список: перечислить весь исходный код для текущей функции или кадра). Это действительно полезно, когда вы перебираете незнакомый код или просто хотите увидеть всю функцию в контексте.

Вот пример:

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) s
--Call--
> /code/example3.py(6)get_path()
-> def get_path(filename):
(Pdb) ll
  6  -> def get_path(filename):
  7         """Return file's path or empty string if no path."""
  8         head, tail = os.path.split(filename)
  9         return head
(Pdb)

Чтобы увидеть более короткий фрагмент кода, используйте командуl (list). Без аргументов он напечатает 11 строк вокруг текущей строки или продолжит предыдущий листинг. Передайте аргумент., чтобы всегда отображать 11 строк вокруг текущей строки:l .

$ ./example3.py
> /code/example3.py(14)()
-> filename_path = get_path(filename)
(Pdb) l
  9         return head
 10
 11
 12     filename = __file__
 13     import pdb; pdb.set_trace()
 14  -> filename_path = get_path(filename)
 15     print(f'path = {filename_path}')
[EOF]
(Pdb) l
[EOF]
(Pdb) l .
  9         return head
 10
 11
 12     filename = __file__
 13     import pdb; pdb.set_trace()
 14  -> filename_path = get_path(filename)
 15     print(f'path = {filename_path}')
[EOF]
(Pdb)

Использование точек останова

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

Используйте командуb (break), чтобы установить точку останова. Вы можете указать номер строки или имя функции, где выполнение остановлено.

Синтаксис для разрыва:

b(reak) [ ([filename:]lineno | function) [, condition] ]

Еслиfilename: не указан перед номером строкиlineno, то используется текущий исходный файл.

Обратите внимание на необязательный второй аргументb:condition. Это очень сильно. Представьте себе ситуацию, когда вы хотели сломаться, только если существовало определенное условие. Если вы передадите выражение Python в качестве второго аргумента, pdb прекратит работу, когда выражение оценивается как true. Мы сделаем это в примере ниже.

В этом примере есть служебный модульutil.py. Давайте установим точку останова для остановки выполнения функцииget_path().

Вот источник основного скриптаexample4.py:

#!/usr/bin/env python3

import util

filename = __file__
import pdb; pdb.set_trace()
filename_path = util.get_path(filename)
print(f'path = {filename_path}')

Вот исходный код служебного модуляutil.py:

def get_path(filename):
    """Return file's path or empty string if no path."""
    import os
    head, tail = os.path.split(filename)
    return head

Во-первых, давайте установим точку останова, используя имя файла источника и номер строки:

$ ./example4.py
> /code/example4.py(7)()
-> filename_path = util.get_path(filename)
(Pdb) b util:5
Breakpoint 1 at /code/util.py:5
(Pdb) c
> /code/util.py(5)get_path()
-> return head
(Pdb) p filename, head, tail
('./example4.py', '.', 'example4.py')
(Pdb)

Командаc (continue) продолжает выполнение, пока не будет найдена точка останова.

Далее, давайте установим точку останова, используя имя функции:

$ ./example4.py
> /code/example4.py(7)()
-> filename_path = util.get_path(filename)
(Pdb) b util.get_path
Breakpoint 1 at /code/util.py:1
(Pdb) c
> /code/util.py(3)get_path()
-> import os
(Pdb) p filename
'./example4.py'
(Pdb)

Введитеb без аргументов, чтобы увидеть список всех точек останова:

(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /code/util.py:1
(Pdb)

Вы можете отключить и снова включить точки останова с помощью командыdisable bpnumber иenable bpnumber. bpnumber - это номер точки останова из 1-го столбцаNum списка точек останова. Обратите внимание на изменение значения столбцаEnb:

(Pdb) disable 1
Disabled breakpoint 1 at /code/util.py:1
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep no    at /code/util.py:1
(Pdb) enable 1
Enabled breakpoint 1 at /code/util.py:1
(Pdb) b
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /code/util.py:1
(Pdb)

Чтобы удалить точку останова, используйте командуcl (clear):

cl(ear) filename:lineno
cl(ear) [bpnumber [bpnumber...]]

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

В этом примере сценария функцияget_path() не работает, когда получает относительный путь, т.е. путь к файлу не начинается с/. Я создам выражение, которое в данном случае будет истинным, и передам егоb в качестве второго аргумента:

$ ./example4.py
> /code/example4.py(7)()
-> filename_path = util.get_path(filename)
(Pdb) b util.get_path, not filename.startswith('/')
Breakpoint 1 at /code/util.py:1
(Pdb) c
> /code/util.py(3)get_path()
-> import os
(Pdb) a
filename = './example4.py'
(Pdb)

После создания указанной выше точки останова и вводаc для продолжения выполнения pdb останавливается, когда выражение оценивается как истина. Командаa (args) печатает список аргументов текущей функции.

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

Если вам нужно прервать использование выражения с именем переменной, расположенного внутри функции, то есть имя переменной отсутствует в списке аргументов функции, укажите номер строки:

$ ./example4.py
> /code/example4.py(7)()
-> filename_path = util.get_path(filename)
(Pdb) b util:5, not head.startswith('/')
Breakpoint 1 at /code/util.py:5
(Pdb) c
> /code/util.py(5)get_path()
-> return head
(Pdb) p head
'.'
(Pdb) a
filename = './example4.py'
(Pdb)

Вы также можете установить временную точку останова с помощью командыtbreak. Он удаляется автоматически при первом попадании. Он использует те же аргументы, что иb.

Продолжая казнь

До сих пор мы рассматривали пошаговое выполнение кода с помощьюn (следующий) иs (шаг) и использование точек останова сb (перерыв) иc (продолжение) .

Также есть связанная команда:unt (до).

Используйтеunt, чтобы продолжить выполнение, какc, но остановитесь на следующей строке, большей, чем текущая. Иногдаunt удобнее и быстрее в использовании, и это именно то, что вам нужно. Я продемонстрирую это на примере ниже.

Давайте сначала посмотрим на синтаксис и описание дляunt:

команда Синтаксис Описание

unt

unt (il) [льняной]

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

В зависимости от того, передаете ли вы аргумент номера строкиlineno,unt может вести себя двумя способами:

  • Безlineno продолжить выполнение до тех пор, пока не будет достигнута строка с номером больше текущего. Это похоже наn (следующий). Это альтернативный способ выполнения и «перешагивания» кода. Разница междуn иunt заключается в том, чтоunt останавливается только при достижении строки с номером, большим, чем текущий. n остановится на следующей логически выполненной строке.

  • Сlineno продолжайте выполнение до тех пор, пока не будет достигнута строка с номером, большим или равным этому. Это похоже наc (continue) с аргументом номера строки.

В обоих случаяхunt останавливается, когда возвращается текущий кадр (функция), точно так же, какn (следующий) иs (шаг).

Основное поведение, которое следует отметить сunt, заключается в том, что он останавливается, когда достигается номер строкиgreater or equal для текущей или указанной строки.

Используйтеunt, если вы хотите продолжить выполнение и остановиться дальше в текущем исходном файле. Вы можете рассматривать его как гибридn (next) иb (break), в зависимости от того, передаете ли вы аргумент номера строки или нет.

В приведенном ниже примере есть функция с циклом. Здесь вы хотите продолжить выполнение кода и остановиться после цикла, не проходя через каждую итерацию цикла или устанавливая точку останова:

Вот пример источника дляexample4unt.py:

#!/usr/bin/env python3

import os


def get_path(fname):
    """Return file's path or empty string if no path."""
    import pdb; pdb.set_trace()
    head, tail = os.path.split(fname)
    for char in tail:
        pass  # Check filename char
    return head


filename = __file__
filename_path = get_path(filename)
print(f'path = {filename_path}')

И вывод консоли с использованиемunt:

$ ./example4unt.py
> /code/example4unt.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) unt
> /code/example4unt.py(10)get_path()
-> for char in tail:
(Pdb)
> /code/example4unt.py(11)get_path()
-> pass  # Check filename char
(Pdb)
> /code/example4unt.py(12)get_path()
-> return head
(Pdb) p char, tail
('y', 'example4unt.py')

Командаll использовалась первой для печати исходного кода функции, а затемunt. pdb запоминает последнюю введенную команду, поэтому я просто нажал[.kbd .key-enter]#Enter #, чтобы повторить командуunt. Это продолжало выполнение кода, пока исходная строка не превысила текущую строку.

Обратите внимание на вывод консоли выше, что pdb остановился только один раз в строках10 и11. Поскольку использовалсяunt, выполнение было остановлено только на 1-й итерации цикла. Однако каждая итерация цикла была выполнена. Это можно проверить в последней строке вывода. Значение переменнойchar'y' равно последнему символу в значенииtail'example4unt.py'.

Отображение выражений

Подобно печати выражений сp иpp, вы можете использовать командуdisplay [expression], чтобы указать pdb автоматически отображать значение выражения, если оно изменилось, при остановке выполнения. Используйте командуundisplay [expression], чтобы очистить отображаемое выражение.

Вот синтаксис и описание для обеих команд:

команда Синтаксис Описание

display

дисплей [выражение]

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

undisplay

unisplay [выражение]

Больше не отображатьexpression в текущем кадре. Безexpression очистить все отображаемые выражения для текущего кадра.

Ниже приведен примерexample4display.py, демонстрирующий его использование с циклом:

$ ./example4display.py
> /code/example4display.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) b 11
Breakpoint 1 at /code/example4display.py:11
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
(Pdb) display char
display char: 'e'
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'x'  [old: 'e']
(Pdb)
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'a'  [old: 'x']
(Pdb)
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'm'  [old: 'a']

В выходных данных выше pdb автоматически отображал значение переменнойchar, потому что каждый раз, когда достигалась точка останова, ее значение изменялось. Иногда это полезно и именно то, что вам нужно, но есть другой способ использоватьdisplay.

Вы можете ввестиdisplay несколько раз, чтобы создать список отслеживания выражений. Это может быть проще в использовании, чемp. После добавления всех интересующих вас выражений просто введитеdisplay, чтобы увидеть текущие значения:

$ ./example4display.py
> /code/example4display.py(9)get_path()
-> head, tail = os.path.split(fname)
(Pdb) ll
  6     def get_path(fname):
  7         """Return file's path or empty string if no path."""
  8         import pdb; pdb.set_trace()
  9  ->     head, tail = os.path.split(fname)
 10         for char in tail:
 11             pass  # Check filename char
 12         return head
(Pdb) b 11
Breakpoint 1 at /code/example4display.py:11
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
(Pdb) display char
display char: 'e'
(Pdb) display fname
display fname: './example4display.py'
(Pdb) display head
display head: '.'
(Pdb) display tail
display tail: 'example4display.py'
(Pdb) c
> /code/example4display.py(11)get_path()
-> pass  # Check filename char
display char: 'x'  [old: 'e']
(Pdb) display
Currently displaying:
char: 'x'
fname: './example4display.py'
head: '.'
tail: 'example4display.py'

Python Caller ID

В этом последнем разделе мы будем опираться на то, что уже узнали, и закончим с хорошей отдачей. Я использую имя «идентификатор звонящего» в связи с функцией идентификации звонящего в телефонной системе. Это именно то, что демонстрирует этот пример, за исключением того, что он применяется к Python.

Вот источник основного скриптаexample5.py:

#!/usr/bin/env python3

import fileutil


def get_file_info(full_fname):
    file_path = fileutil.get_path(full_fname)
    return file_path


filename = __file__
filename_path = get_file_info(filename)
print(f'path = {filename_path}')

Вот служебный модульfileutil.py:

def get_path(fname):
    """Return file's path or empty string if no path."""
    import os
    import pdb; pdb.set_trace()
    head, tail = os.path.split(fname)
    return head

В этом сценарии представьте, что имеется большая база кода с функцией в служебном модулеget_path(), которая вызывается с недопустимым вводом. Однако он вызывается из разных мест в разных пакетах.

Как узнать, кто звонит?

Используйте командуw (где), чтобы распечатать трассировку стека, с самым последним кадром внизу:

$ ./example5.py
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/example5.py(12)()
-> filename_path = get_file_info(filename)
  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb)

Не беспокойтесь, если это выглядит сбивающим с толку или если вы не уверены, что такое трассировка стека или фрейм. Я объясню эти термины ниже. Это не так сложно, как может показаться.

Поскольку самый последний кадр находится внизу, начните там и читайте снизу вверх. Посмотрите на строки, начинающиеся с->, но пропустите 1-й экземпляр, поскольку именно тамpdb.set_trace() использовался для ввода pdb в функциюget_path(). В этом примере исходная строка, вызывающая функциюget_path():

-> file_path = fileutil.get_path(full_fname)

Строка над каждым-> содержит имя файла, номер строки (в скобках) и имя функции, в которой находится исходная строка. Итак, вызывающий абонент:

  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)

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

Теперь мы знаем, как найти звонящего.

Но как насчет трассировки стека и фреймов?

Трассировка стека - это просто список всех фреймов, которые Python создал для отслеживания вызовов функций. Фрейм - это структура данных, которую Python создает при вызове функции, и удаляет при возврате. Стек - это просто упорядоченный список кадров или вызовов функций в любой момент времени. Стек (вызов функции) растет и сжимается в течение всего жизненного цикла приложения при вызове функций и их возврате.

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

См. Подробности в этомcall stack article on Wikipedia.

Чтобы лучше понять и получить больше от pdb, давайте более внимательно рассмотрим справку дляw:

(Pdb) h w
w(here)
        Print a stack trace, with the most recent frame at the bottom.
        An arrow indicates the "current frame", which determines the
        context of most commands. 'bt' is an alias for this command.

Что pdb означает «текущий кадр»?

Думайте о текущем кадре как о текущей функции, в которой pdb прекратил выполнение. Другими словами, текущий фрейм - это то место, где ваше приложение в настоящее время приостановлено, и используется как «фрейм» для команд pdb, таких какp (print).

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

Когда pdb печатает трассировку стека, стрелка> указывает текущий кадр.

Чем это полезно?

Вы можете использовать две командыu (вверх) иd (вниз), чтобы изменить текущий кадр. В сочетании сp это позволяет вам проверять переменные и состояние в вашем приложении в любой точке стека вызовов в любом кадре.

Вот синтаксис и описание для обеих команд:

команда Синтаксис Описание

u

u (p) [количество]

Переместить текущий кадрcount (по умолчанию) на уровни вверх в трассировке стека (к более старому кадру).

d

d (собственный) [количество]

Переместить текущий кадрcount (по умолчанию) на уровни вниз в трассировке стека (к более новому кадру).

Давайте посмотрим на пример с использованием командu иd. В этом сценарии мы хотим проверить переменнуюfull_fname, которая является локальной для функцииget_file_info() вexample5.py. Для этого мы должны изменить текущий кадр на один уровень выше, используя командуu:

$ ./example5.py
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/example5.py(12)()
-> filename_path = get_file_info(filename)
  /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) u
> /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)
(Pdb) p full_fname
'./example5.py'
(Pdb) d
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) p fname
'./example5.py'
(Pdb)

Вызовpdb.set_trace() находится вfileutil.py в функцииget_path(), поэтому текущий кадр изначально устанавливается там. Вы можете увидеть это в 1-й строке вывода выше:

> /code/fileutil.py(5)get_path()

Чтобы получить доступ и распечатать локальную переменнуюfull_fname в функцииget_file_info() вexample5.py, была использована командаu для перехода на один уровень вверх:

(Pdb) u
> /code/example5.py(7)get_file_info()
-> file_path = fileutil.get_path(full_fname)

Обратите внимание на выводu выше, что pdb напечатал стрелку> в начале 1-й строки. Это pdb, позволяющий вам знать, что кадр был изменен, и это местоположение источника теперь является текущим кадром. Переменнаяfull_fname теперь доступна. Также важно понимать, что исходная строка, начинающаяся с-> во 2-й строке, была выполнена. Поскольку кадр был перемещен вверх по стеку, был вызванfileutil.get_path(). Используяu, мы переместились вверх по стеку (в некотором смысле назад во времени) к функцииexample5.get_file_info(), где был вызванfileutil.get_path().

Продолжая пример, после того, какfull_fname был напечатан, текущий кадр был перемещен в исходное положение с использованиемd, и была напечатана локальная переменнаяfname вget_path().

Если бы мы хотели, мы могли бы переместить несколько кадров одновременно, передав аргументcount вu илиd. Например, мы могли перейти на уровень модуля вexample5.py, введяu 2:

$ ./example5.py
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) u 2
> /code/example5.py(12)()
-> filename_path = get_file_info(filename)
(Pdb) p filename
'./example5.py'
(Pdb)

Легко забыть, где вы находитесь, когда вы отлаживаете и думаете о многих разных вещах. Просто помните, что вы всегда можете использовать команду с подходящим названиемw (where), чтобы увидеть, где приостановлено выполнение и какой текущий кадр.

Основные команды pdb

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

Просто введитеh илиhelp <topic>, чтобы получить список всех команд или справку по конкретной команде или теме.

Для быстрого ознакомления, вот список основных команд:

команда Описание

p

Распечатайте значение выражения.

pp

Достаточно напечатать значение выражения.

n

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

s

Выполнить текущую строку и остановиться при первой возможности (либо в вызываемой функции, либо в текущей функции).

c

Продолжить выполнение и останавливаться только при обнаружении точки останова.

unt

Продолжайте выполнение до тех пор, пока не дойдете до строки с номером больше текущего. С аргументом номера строки продолжайте выполнение до тех пор, пока не будет достигнута строка с номером, большим или равным этому.

l

Вывести исходный код для текущего файла. Без аргументов перечислите 11 строк вокруг текущей строки или продолжите предыдущий список.

ll

Перечислите весь исходный код для текущей функции или фрейма.

b

Без аргументов перечислить все перерывы. С аргументом номера строки установите точку останова на этой строке в текущем файле.

w

Распечатайте трассировку стека с самым последним кадром внизу. Стрелка указывает текущий фрейм, который определяет контекст большинства команд.

u

Переместить текущий счетчик кадров (по умолчанию) на более высокий уровень в трассировке стека (к более старому кадру).

d

Переместите текущий счетчик кадров (по умолчанию) на уровни вниз в трассировке стека (к более новому кадру).

h

См. Список доступных команд.

h

Показать справку по команде или теме.

h pdb

Покажите полную документацию pdb.

q

Закройте отладчик и выйдите.

Отладка Python с помощью pdb: Заключение

В этом уроке мы рассмотрели несколько основных и распространенных способов использования pdb:

  • печать выражений

  • пошаговое выполнение кода с помощьюn (следующий) иs (шаг)

  • используя точки останова

  • продолжение выполнения сunt (до)

  • отображение выражений

  • найти вызывающего функцию

Я надеюсь, что это было полезно для вас. Если вам интересно узнать больше, смотрите:

Исходный код, используемый в примерах, можно найти на связанномGitHub repository. Обязательно ознакомьтесь с нашим справочником команд для печати pdb, который вы можете использовать в качестве шпаргалки при отладке:

Free Bonus:Click here to get a printable "pdb Command Reference" (PDF), который вы можете держать на столе и использовать во время отладки.

Кроме того, если вы хотите попробовать отладчик Python на основе графического интерфейса пользователя, прочтите нашPython IDEs and Editors Guide, чтобы узнать, какие варианты лучше всего подходят для вас. Счастливого Pythoning!