Отладчик pdb

Инструменты отладки — сердце любого языка программирования. Без них вам будет крайне сложно написать рабочий и чистый код. В этой статье мы познакомим вас с одним из таких инструментов — Python-отладчиком pdb (англ. Python Debugger (pdb)).

Обратите внимание: перед вами руководство по отладке кода. Мы исходим из того, что вы знакомы с Python и имеете представление о написании тест-кейсов.

От редакции Pythonist. Также предлагаем почитать о таком средстве дебага, как модуль icecream — «Забудьте об использовании print() для отладки в Python».

Отладчик pdb: начало работы

Вызвать pdb можно двумя способами.

1. Вызов pdb извне

Находясь в терминале, вы можете вызвать pdb при запуске на выполнение вашего файла с расширением .py.

python -m pdb <test-file-name>.py

Если вы используете poetry и pytest, вы можете вызвать отладчик pdb, добавив в конце флаг --pdb.

poetry run python <path/to_your/test_file.py> --pdb

Для вызова pdb при помощи Docker, poetry и pytest можно использовать следующий синтаксис:

COMPOSE_PROJECT_NAME=<test_docker_image_name> docker-compose run --rm workers poetry run pytest <path/to_your/test_file.py>::<name_of_the_test_function> --pdb

Флаг --pdb всегда добавляется после имени тестового файла. Когда тест прервется, откроется консоль pdb. Но помните, что --pdb — это флаг pytest.

2. Добавление брейкпоинта при помощи pdb

Бывает, что вы получаете ложнопозитивный результат теста. Ваш тест-кейс может пройти, но вы не получаете ожидаемых данных.

Что, если вы хотите прочесть необработанный запрос к базе данных? В этом случае вы можете вызвать отладчик pdb из функции Python.

Чтобы войти в отладчик pdb, нужно вызвать import pdb; pdb.set_trace() внутри вашей функции.

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

# file1.py

from . import function3

def function1():
    // logic for function1
    function3()
# file2.py

def function2():
    // logic for function2
    // some database query
# file3.py

from . import function2

def function3():
    // logic for function3
    function2()
    // logic for function3 continues

В этом примере одна функция вызывает другую.

Вы хотите добавить брейкпоинт в функцию function2, чтобы посмотреть, что в ней происходит.

Добавить брейкпоинт можно при помощи следующей инструкции:

import pdb; pdb.set_trace()

# file2.py

1. def function2():
2.     // logic for function2
3.     // line 1
4.     // line 2
5.    
6.     import pdb; pdb.set_trace();
7.    
8.     // some database query
9.     // line 3
10.    // line 4

Когда код ломается, pdb открывает свою консоль. Примерно так:

(Pdb)

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

Распространенные команды pdb

Отладчик pdb — это интерактивная консольная утилита. Не познакомившись с его командами, вы не сможете использовать весь его потенциал.

Как и любой console log, pdb скажет вам, на какой строчке ваш код сломался.

Команда print

Допустим, в вашем тест-кейсе есть инструкция assert.

# test.py
    
def test1():
    ...
    result = function1()
    assert result.json = {'status_code':1, 'status': 'saved', 'description':'data saved'}

Для вывода значения в консоль используется команда p.

(Pdb) p result.json
{'status_code':1, 'status': 'saved', 'description':'data saved'}

Таким образом будет выведено значение переменной.

Команда up

Команда up перемещает вас на один шаг выше по стеку.

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

Пример:

# test.py

def function1():
    print("invoking function1")
    import pdb;pdb.set_trace()
    print("function1 invoked")


def function2():
    print("invoking function2")
    function1()
    print("function2 invoked")


def function3():
    print("inside function3")
    function2()
    print("function3 invoked")

# starting the call with function2()
 
function3()

В pdb вызов происходит так:

$ python -m pdb test.py

> test.py(1)<module>()
-> def function1():
(Pdb) n
> test.py(7)<module>()
-> def function2():
(Pdb) n
> test.py(13)<module>()
-> def function3():
(Pdb) n
> test.py(20)<module>()
-> function3()
(Pdb) n
inside function3
invoking function2
invoking function1
> test.py(4)function1()
-> print("function1 invoked")
(Pdb) n
function1 invoked
--Return--
> test.py(4)function1()->None
-> print("function1 invoked")
(Pdb) u
> test.py(9)function2()
-> function1()
(Pdb) l
  4         print("function1 invoked")
  5
  6
  7     def function2():
  8         print("invoking function2")
  9  ->     function1()
 10         print("function2 invoked")
 11
 12
 13     def function3():
 14         print("inside function3")
(Pdb) u
> test.py(15)function3()
-> function2()
(Pdb) l
 10         print("function2 invoked")
 11
 12
 13     def function3():
 14         print("inside function3")
 15  ->     function2()
 16         print("function3 invoked")
 17
 18     # starting the call with function2()
 19
 20     function3()
(Pdb) u
> test.py(20)<module>()
-> function3()
(Pdb) l
 15         function2()
 16         print("function3 invoked")
 17
 18     # starting the call with function2()
 19
 20  -> function3()
[EOF]
(Pdb) u
> <string>(1)<module>()

Здесь мы начинаем с вызова function3(). Выполнение останавливается, когда доходит до import pdb.

pdb открывает консоль и ожидает инпут. Мы вводим u для перемещения вверх, и pdb возвращает вызываемую функцию — function2(). Еще один ввод команды u вернет function3 (функцию, которая вызывает function2).

Мы также используем команду list (l). Она выводит местонахождение строки, которая выполняется в настоящее время.

Команда step

Чтобы понять команду step, давайте продолжим разбирать предыдущий пример.

# test.py
    
1. def test1():
2.    ...
3.    result = function1()
4.    assert result.json.status_code == 1
5.    assert result.json.status == 'saved'
6.    assert result.json.description == 'data saved'
7.
# function_file.py

def function1():
    foo = ['bar']
    ...

Вы подозреваете, что результат, который вернула function1(), неправильный. Ваш код ломается на строке 6. Как вам переместиться на строку 3?

Сначала нужно использовать команду up, а в конце войти в функцию при помощи команды s.

(Pdb) u
assert result.json.status == 'saved'
(Pdb) u
assert result.json.status_code == 1
(Pdb) p assert result.json.status_code
1
(Pdb) u
result = function1()
(Pdb) s
(Pdb) n
foo = ['bar']

При входе в function1() консоль pdb начнет выводить предложения из этой функции.

Заключение

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

Перевод статьи «How to Debug Your Python Code with the Python Debugger (pdb)».