Рубрики
Без рубрики

Расширенная отладка Python с pdb

Отлаживайте свой код Python быстрее с помощью этих советов pdb.

Автор оригинала: Steven Kryskalla.

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

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

Отладка печати по сравнению с интерактивной отладкой

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

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

Кроме того, не касаясь базового исходного кода, вы сможете перейти к стороннему коду (например, модулям, установленным из PyPI) и стандартной библиотеке.

Посмертная отладка

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

Вы можете запустить существующий сценарий с помощью отладчика post-mortem, используя параметр Python -pdb :

python3 -mpdb path/to/script.py

Отсюда вы попадаете в приглашение (Pdb) . Чтобы начать выполнение, вы используете команду continue или c . Если программа будет выполнена успешно, вы вернетесь в приглашение (Pdb) , где вы сможете снова перезапустить выполнение. На этом этапе вы можете использовать quit / q или Ctrl+D для выхода из отладчика.

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

Тестирование нашего основного рабочего процесса

Чтобы увидеть, как работают эти основные шаги отладки, я буду использовать эту (ошибочную) программу:

import random

MAX = 100

def main(num_loops=1000):
    for i in range(num_loops):
        num = random.randint(0, MAX)
        denom = random.randint(0, MAX)
        result = num / denom
        print("{} divided by {} is {:.2f}".format(num, denom, result))

if __name__ == "__main__":
    import sys
    arg = sys.argv[-1]
    if arg.isdigit():
        main(arg)
    else:
        main()

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

$ python3 script.py
2 divided by 30 is 0.07
65 divided by 41 is 1.59
0 divided by 70 is 0.00
...
38 divided by 26 is 1.46
Traceback (most recent call last):
  File "script.py", line 16, in 
    main()
  File "script.py", line 7, in main
    result = num / denom
ZeroDivisionError: division by zero

Давайте попробуем посмертно отладить эту ошибку:

$ python3 -mpdb script.py
> ./src/script.py(1)()
-> import random
(Pdb) c
49 divided by 46 is 1.07
...
Traceback (most recent call last):
  File "/usr/lib/python3.4/pdb.py", line 1661, in main
    pdb._runscript(mainpyfile)
  File "/usr/lib/python3.4/pdb.py", line 1542, in _runscript
    self.run(statement)
  File "/usr/lib/python3.4/bdb.py", line 431, in run
    exec(cmd, globals, locals)
  File "", line 1, in 
  File "./src/script.py", line 1, in 
    import random
  File "./src/script.py", line 7, in main
    result = num / denom
ZeroDivisionError: division by zero
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> ./src/script.py(7)main()
-> result = num / denom
(Pdb) num
76
(Pdb) denom
0
(Pdb) random.randint(0, MAX)
56
(Pdb) random.randint(0, MAX)
79
(Pdb) random.randint(0, 1)
0
(Pdb) random.randint(1, 1)
1

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

Переход в отладчик из кода Python с помощью pdb.set_trace

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

Например, в приведенном выше коде, если мы хотим остановить выполнение перед операцией деления, мы могли бы добавить вызов pdb.set_trace в нашу программу здесь:

    import pdb; pdb.set_trace()
    result = num / denom

А затем запустите нашу программу без -mpdb :

$ python3 script.py
> ./src/script.py(10)main()
-> result = num / denom
(Pdb) num
94
(Pdb) denom
19

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

Использование pdb.set_trace позволяет выполнить работу, но точки останова – это еще более гибкий способ остановить отладчик в любой строке (даже в коде сторонних разработчиков или стандартной библиотеки) без необходимости изменять исходный код. Давайте узнаем о точках останова и нескольких других полезных командах.

Команды отладчика

Существует более 30 команд, которые вы можете дать интерактивному отладчику, список которых можно просмотреть с помощью команды help в командной строке (Pdb) :

(Pdb) help

Documented commands (type help ):
========================================
EOF    c          d        h         list      q        rv       undisplay
a      cl         debug    help      ll        quit     s        unt
alias  clear      disable  ignore    longlist  r        source   until
args   commands   display  interact  n         restart  step     up
b      condition  down     j         next      return   tbreak   w
break  cont       enable   jump      p         retval   u        whatis
bt     continue   exit     l         pp        run      unalias  where

Вы можете использовать help для получения дополнительной информации о данной команде.

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

Установка точек останова :

  • l(ist) : отображает исходный код текущей запущенной программы с номерами строк для 10 строк вокруг текущего оператора.
  • l 1,999 : отображает исходный код строк 1-999. Я регулярно использую это, чтобы увидеть источник для всей программы. Если в вашей программе всего 20 строк, она просто покажет все 20 строк.
  • b(точка останова) : отображает список текущих точек останова.
  • b 10 : установите точку останова в строке 10. Точки останова обозначаются числовым идентификатором, начинающимся с 1.
  • b main : установите точку останова в функции с именем main . Имя функции должно находиться в текущей области. Вы также можете установить точки останова для функций в других модулях текущей области, например b random.randint .
  • b script.py:10 : устанавливает точку останова в строке 10 в script.py . Это дает вам еще один способ установить точки останова в другом модуле.
  • очистить : очищает все точки останова.
  • очистить 1 : очистить точку останова 1.

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

  • c(продолжить) : выполнение до тех пор, пока программа не завершится, не возникнет исключение или не будет достигнута точка останова.
  • s(tep) : выполните следующую строку, что бы это ни было (ваш код, stdlib, сторонний код и т. Д.). Используйте это, когда вы хотите перейти к вызовам функций, которые вас интересуют.
  • n(ext) : выполните следующую строку в текущей функции (не будет переходить в последующие вызовы функций). Используйте это, когда вас интересует только текущая функция.
  • r(eturn) : выполняйте оставшиеся строки в текущей функции до тех пор, пока она не вернется. Используйте это, чтобы пропустить остальную часть функции и подняться на уровень выше. Например, если вы по ошибке перешли в функцию.
  • unt(il) [lineno] : выполнение до тех пор, пока текущая строка не превысит номер текущей строки. Это полезно, когда вы вступили в цикл, но хотите, чтобы цикл продолжал выполняться без необходимости вручную проходить каждую итерацию. Без каких-либо аргументов эта команда ведет себя как next (с поведением пропуска цикла, как только вы один раз прошли через тело цикла).

Перемещение вверх и вниз по стеку :

  • w(здесь) : показывает аннотированное представление трассировки стека с текущим кадром, отмеченным символом > .
  • u(p) : перемещение вверх на один кадр в текущей трассировке стека. Например, при посмертной отладке вы начнете с самого низкого уровня стека и, как правило, захотите несколько раз переместить вверх , чтобы выяснить, что пошло не так.
  • d(собственный) : перемещение вниз на один кадр в текущей трассировке стека.

Дополнительные команды и советы :

  • pp : Это “довольно напечатает” результат данного выражения с помощью модуля pprint . Пример:
(Pdb) stuff = "testing the pp command in pdb with a big list of strings"
(Pdb) pp [(i, x) for (i, x) in enumerate(stuff.split())]
[(0, 'testing'),
 (1, 'the'),
 (2, 'pp'),
 (3, 'command'),
 (4, 'in'),
 (5, 'pdb'),
 (6, 'with'),
 (7, 'a'),
 (8, 'big'),
 (9, 'list'),
 (10, 'of'),
 (11, 'strings')]
  • !<код python> : иногда код Python, который вы запускаете в отладчике, будет перепутан с командой. Например, c вызовет команду continue . Чтобы заставить отладчик выполнять код Python, префикс строки ! , например !c .

  • Нажатие клавиши Enter в командной строке (Pdb) приведет к повторному выполнению предыдущей команды. Это наиболее полезно после команд s / n / r / unt для быстрого пошагового выполнения построчно.

  • Вы можете выполнить несколько команд в одной строке , разделив их с помощью ;; , например b 8 ;; c .

  • Модуль pdb может принимать несколько аргументов -c в командной строке для выполнения команд сразу после запуска отладчика.

Пример:

python3 -mpdb -cc script.py # run the program without you having to enter an initial "c" at the prompt
python3 -mpdb -c "b 8" -cc script.py # sets a breakpoint on line 8 and runs the program

Поведение перезапуска

Еще одна вещь, которая может сократить время отладки, – это понимание того, как работает поведение перезапуска pdb . Возможно, вы заметили, что после остановки выполнения pdb выдаст сообщение типа “Программа завершена и будет перезапущена” или “Скрипт будет перезапущен.” Когда я впервые начал использовать pdb , я всегда выходил и повторно запускал python -npdb ... чтобы убедиться, что мои изменения в коде были приняты, что в большинстве случаев было излишним.

Когда pdb говорит, что он перезапустит программу, или когда вы используете команду restart , изменения кода в отлаживаемом скрипте будут автоматически перезагружены. Точки останова по-прежнему будут установлены после перезагрузки, но, возможно, потребуется очистить и сбросить их из-за смещения номеров строк. Изменения кода в другие импортированные модули не будут перезагружены — вам нужно будет выйти и повторно запустить команду -mpdb , чтобы забрать их.

Часы

Одна из функций, которую вы можете пропустить в других интерактивных отладчиках, – это возможность “наблюдать” за изменением переменной на протяжении всего выполнения программы. pdb по умолчанию не включает команду watch, но вы можете получить нечто подобное с помощью commands , которая позволяет запускать произвольный код Python при попадании в точку останова.

Чтобы посмотреть, что происходит с переменной denom в нашем примере программы:

$ python3 -mpdb script.py
> ./src/script.py(1)()
-> import random
(Pdb) b 9
Breakpoint 1 at ./src/script.py:9
(Pdb) commands
(com) silent
(com) print("DENOM: {}".format(denom))
(com) c
(Pdb) c
DENOM: 77
71 divided by 77 is 0.92
DENOM: 27
100 divided by 27 is 3.70
DENOM: 10
82 divided by 10 is 8.20
DENOM: 20
...

Сначала мы устанавливаем точку останова (которой присваивается идентификатор 1), а затем используем команды , чтобы начать ввод блока команд. Эти команды функционируют так, как если бы вы ввели их в командной строке (Pdb) . Они могут быть либо кодом Python, либо дополнительными командами pdb .

Как только мы запустим блок commands , приглашение изменится на (com) . Команда silent означает, что следующие команды не будут воспроизводиться на экране каждый раз, когда они выполняются, что немного облегчает чтение выходных данных.

После этого мы запускаем инструкцию print для проверки переменной, аналогично тому, что мы могли бы сделать при отладке print . Наконец, мы заканчиваем a c , чтобы продолжить выполнение, которое завершает командный блок. Набрав c снова в (Pdb) приглашение начинает выполнение, и мы видим, что наш новый print оператор запущен.

Если вы предпочитаете остановить выполнение вместо продолжения, вы можете использовать end вместо c в блоке команд.

Запуск pdb из интерпретатора

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

Для посмертной отладки все, что вам нужно,-это позвонить в pdb.pm() после возникновения исключения:

$ python3
>>> import script
>>> script.main()
17 divided by 60 is 0.28
...
56 divided by 94 is 0.60
Traceback (most recent call last):
  File "", line 1, in 
  File "./src/script.py", line 9, in main
    result = num / denom
ZeroDivisionError: division by zero
>>> import pdb
>>> pdb.pm()
> ./src/script.py(9)main()
-> result = num / denom
(Pdb) num
4
(Pdb) denom
0

Если вы хотите вместо этого перейти к обычному выполнению, используйте функцию pdb.run() :

$ python3
>>> import script
>>> import pdb
>>> pdb.run("script.main()")
> (1)()
(Pdb) b script:6
Breakpoint 1 at ./src/script.py:6
(Pdb) c
> ./src/script.py(6)main()
-> for i in range(num_loops):
(Pdb) n
> ./src/script.py(7)main()
-> num = random.randint(0, MAX)
(Pdb) n
> ./src/script.py(8)main()
-> denom = random.randint(0, MAX)
(Pdb) n
> ./src/script.py(9)main()
-> result = num / denom
(Pdb) n
> ./src/script.py(10)main()
-> print("{} divided by {} is {:.2f}".format(num, denom, result))
(Pdb) n
66 divided by 70 is 0.94
> ./src/script.py(6)main()
-> for i in range(num_loops):

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

Вывод

Надеюсь, эти советы дали вам несколько новых идей о том, как использовать pdb более эффективно. После того, как вы справитесь с ними, вы сможете выбрать другие команды и начать настройку pdb через файл .pdbrc ( пример ).

Вы также можете изучить другие интерфейсы для отладки, такие как pdbpp , pdb и pdb , или отладчики графического интерфейса , такие как тот, который включен в PyCharm. Счастливой отладки!