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

Оглавление

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

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

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

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

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

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

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

В конце этого руководства есть краткая справка по Основным командам pdb.

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

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

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

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

import pdb; pdb.set_trace()

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

Начиная с версии Python 3.7, появился еще один способ входа в отладчик. 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. В конце этого учебника приведен список основных команд pdb. Сейчас давайте воспользуемся командой 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)<module>()
-> 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 (интерфейс командной строки), обратите внимание на символы и форматирование. Они дадут вам необходимый контекст:

  • > начинает 1-ю строку и сообщает, в каком исходном файле вы находитесь. После имени файла в круглых скобках указан номер текущей строки. Далее идет имя функции. В данном примере, так как мы не приостанавливаемся внутри функции и находимся на уровне модуля, мы видим <module>().
  • -> начинается со 2-й строки и является текущей строкой исходного текста, на которой Python приостановлен. Эта строка еще не была выполнена. В данном примере это строка 5 в example1.py, из строки > выше.
  • (Pdb) - это приглашение pdb. Он ожидает команды.

Используйте команду q для прекращения отладки и выхода.

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

При использовании команды print 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
<function get_path at 0x100760e18>
(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) 

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

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

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

Шаг за шагом через код

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

Command Description
n (next) Продолжайте выполнение до тех пор, пока не будет достигнута следующая строка в текущей функции или она не вернется.
s (step) Выполняет текущую строку и останавливается в первом возможном месте (либо в вызываемой функции, либо в текущей функции).

Существует третья команда unt (until). Она связана с n (next). Мы рассмотрим ее позже в этом учебнике в разделе Continuing Execution.

Разница между n (next) и s (step) - это место, где pdb останавливается.

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

Используйте s (step) для выполнения текущей строки и остановки в посторонней функции, если она вызвана. Считайте, что 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)<module>()
-> filename_path = get_path(filename)
(Pdb) n
> /code/example3.py(15)<module>()
-> print(f'path = {filename_path}')
(Pdb) 

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

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

$ ./example3.py 
> /code/example3.py(14)<module>()
-> 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 запоминает вашу последнюю команду. Если вы просматриваете много кода, вы можете просто нажать Enter, чтобы повторить последнюю команду.

Ниже приведен пример использования s и n для пошагового выполнения кода. I enter s initially because I want to “step into” the function get_path() and stop. Then I enter n once to “stay local” or “step over” any other function calls and just press Enter to repeat the n command until I get to the last source line.

$ ./example3.py 
> /code/example3.py(14)<module>()
-> 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)<module>()
-> print(f'path = {filename_path}')
(Pdb) 
path = .
--Return--
> /code/example3.py(15)<module>()->None
-> print(f'path = {filename_path}')
(Pdb) 

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

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

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

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

Перечисление исходного кода

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

Вот пример:

$ ./example3.py 
> /code/example3.py(14)<module>()
-> 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)<module>()
-> 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). Вы можете указать номер строки или имя функции, на которой будет остановлено выполнение.

Синтаксис для break следующий:

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

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

Обратите внимание на необязательный 2-й аргумент b: condition. Это очень мощный аргумент. Представьте себе ситуацию, когда вы хотите прерывать работу только при наличии определенного условия. Если вы передадите выражение Python в качестве 2-го аргумента, 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)<module>()
-> 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 (продолжить) продолжает выполнение до тех пор, пока не будет найдена точка останова.

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

$ ./example4.py 
> /code/example4.py(7)<module>()
-> 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 (очистить):

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

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

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

$ ./example4.py 
> /code/example4.py(7)<module>()
-> 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 остановится, когда выражение будет оценено как true. Команда a (args) выводит список аргументов текущей функции.

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

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

$ ./example4.py 
> /code/example4.py(7)<module>()
-> 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 (next) и s (step) и использование точек останова с помощью b (break) и c (continue).

Есть также родственная команда: unt (until).

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

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

Command Syntax Description
unt unt(il) [lineno] Без lineno, продолжайте выполнение до тех пор, пока не будет достигнута строка с номером, большим, чем текущий. С lineno - продолжать выполнение до тех пор, пока не будет достигнута строка с номером, большим или равным этому. В обоих случаях также остановитесь, когда вернется текущий кадр.

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

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

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

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

Используйте 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 запоминает последнюю введенную команду, поэтому я просто нажал Enter, чтобы повторить команду unt. Это продолжало выполнение кода до тех пор, пока не была достигнута строка исходного текста, превышающая текущую строку.

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

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

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

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

Command Syntax Description
display display [expression] Отображает значение выражения, если оно изменилось, каждый раз, когда выполнение останавливается в текущем кадре. Без выражения - список всех отображаемых выражений для текущего кадра.
undisplay undisplay [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

В последнем разделе мы разберем все, что узнали до сих пор, и закончим приятными хлопотами. Я использую название "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 (where) для печати трассировки стека, с самым последним кадром внизу:

$ ./example5.py 
> /code/fileutil.py(5)get_path()
-> head, tail = os.path.split(fname)
(Pdb) w
  /code/example5.py(12)<module>()
-> 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, как мы делали это выше, чтобы найти вызывающую программу.

Подробнее см. в этой статье Википедии о стеке вызовов.

Чтобы лучше понять и получить больше пользы от 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 это позволяет вам проверять переменные и состояние вашего приложения в любой точке стека вызовов в любом кадре.

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

Command Syntax Description
u u(p) [count] Переместите текущий счетчик кадров (по умолчанию один) на уровень вверх в трассировке стека (на более старый кадр).
d d(own) [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)<module>()
-> 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)<module>()
-> filename_path = get_file_info(filename)
(Pdb) p filename
'./example5.py'
(Pdb) 

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

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

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

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

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

Command Description
p Print the value of an expression.
pp Pretty-print the value of an expression.
n Continue execution until the next line in the current function is reached or it returns.
s Execute the current line and stop at the first possible occasion (either in a function that is called or in the current function).
c Continue execution and only stop when a breakpoint is encountered.
unt Continue execution until the line with a number greater than the current one is reached. With a line number argument, continue execution until a line with a number greater or equal to that is reached.
l List source code for the current file. Without arguments, list 11 lines around the current line or continue the previous listing.
ll List the whole source code for the current function or frame.
b With no arguments, list all breaks. With a line number argument, set a breakpoint at this line in the current file.
w 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.
u Move the current frame count (default one) levels up in the stack trace (to an older frame).
d Move the current frame count (default one) levels down in the stack trace (to a newer frame).
h See a list of available commands.
h <topic> Show help for a command or topic.
h pdb Show the full pdb documentation.
q Quit the debugger and exit.

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

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

  • печать выражений
  • прохождение по коду с помощью n (next) и s (step)
  • использование точек останова
  • продолжение выполнения с помощью unt (until)
  • отображение выражений
  • поиск вызывающей функции

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

  • Полная документация по pdb в ближайшем к вам окне pdb: (Pdb) h pdb
  • Python's pdb docs

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

Бесплатный бонус: Нажмите здесь, чтобы получить распечатанный "Справочник команд pdb" (PDF), который можно держать на столе и обращаться к нему во время отладки.

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

Смотреть сейчас К этому уроку прилагается видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Python Debugging With pdb

Вернуться на верх