REPL и отладчик: ipython, pdb, breakpoint

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

ipython - улучшенный REPL

Базовый python3 REPL аскетичен: нет цветов, плохое автодополнение, не запоминает многострочный ввод. ipython это исправляет:

pip install ipython
ipython

Что даёт:

  • Автодополнение по Tab для имён переменных, методов, путей
  • Цветная подсветка ввода и трейсбэков
  • Введение многострочных блоков без отступов вручную (он сам форматирует)
  • Magic-команды, начинающиеся с %
  • История между сессиями (Up/Down стрелки)
  • Help через ? и исходник через ??

Пример сессии:

In [1]: data = {"name": "Alice", "age": 30}

In [2]: data.<TAB>
        clear()    fromkeys() items()     pop()      setdefault() values()
        copy()     get()      keys()      popitem()  update()

In [3]: data.keys?
Signature: data.keys()
Docstring: D.keys() -> a set-like object providing a view on D's keys
Type:      builtin_function_or_method

In [4]: import requests

In [5]: requests.get??
[выводит исходник функции]

Magic-команды ipython

Команды, начинающиеся с % (line magic) или %% (cell magic):

КомандаЧто делает
%timeit exprЗамеряет время выполнения выражения
%run script.pyЗапускает скрипт в текущем окружении
%load script.pyЗагружает код скрипта в текущую ячейку
%editОткрывает редактор, после сохранения выполняет код
%who / %whosСписок объявленных переменных
%resetСбрасывает все переменные
%pasteВставляет из буфера обмена, разбираясь с отступами
%cd pathМеняет рабочую директорию
%historyИстория команд

%timeit особенно полезен для микро-бенчмарков:

In [1]: %timeit sum(range(1000))
8.42 µs ± 23.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [2]: %timeit [x for x in range(1000)]
13.5 µs ± 142 ns per loop
ipython - это родитель Jupyter Notebook. Большинство фич (magic-команды, автодополнение) пришли оттуда. Если придётся работать с notebook-ами, эти команды пригодятся в том же виде.

Скрипт + REPL: python3 -i

Полезный трюк - запустить скрипт и остаться в REPL с его контекстом:

python3 -i script.py

Скрипт выполняется как обычно, но после завершения вместо выхода открывается REPL, в котором доступны все переменные и функции скрипта:

# script.py
def greet(name):
    return f"Привет, {name}!"

users = ["Alice", "Bob"]
$ python3 -i script.py
>>> users
['Alice', 'Bob']
>>> greet("Carol")
'Привет, Carol!'

Это удобно для проверки результатов: запустил программу - и можно покопаться в её состоянии, не добавляя print() повсюду.

breakpoint() - встроенная точка останова

С Python 3.7 в языке есть встроенная функция breakpoint(). Вставляешь её в код - и интерпретатор останавливается в этой точке, открывая отладчик:

def compute_total(items):
    total = 0
    for item in items:
        breakpoint()  # остановится здесь на каждой итерации
        total += item.price
    return total

При запуске откроется отладчик (по умолчанию pdb):

> /path/script.py(4)compute_total()
-> total += item.price
(Pdb)

Внутри отладчика можно:

(Pdb) p item              # печатает значение item
(Pdb) p item.__dict__     # все атрибуты объекта
(Pdb) n                   # next - следующая строка
(Pdb) s                   # step - зайти внутрь функции
(Pdb) c                   # continue - продолжить до следующей breakpoint()
(Pdb) l                   # list - показать код вокруг
(Pdb) w                   # where - стек вызовов
(Pdb) q                   # quit - выйти
Случайный коммит с `breakpoint()` в продакшен код повесит запрос навсегда (или до таймаута). Многие линтеры (flake8, ruff) ловят `breakpoint()` через правило `T100` - как их настроить, смотри в уроке про [линтеры и типы](./47-linters-and-types.md). В CI стоит добавить проверку чтобы это никогда не попадало в main.

pdb - стандартный отладчик

breakpoint() запускает pdb (Python Debugger) из стандартной библиотеки. Его можно запустить и явно:

python3 -m pdb script.py

Программа остановится на первой строке. Все команды pdb доступны.

Удобные команды pdb:

КомандаПолнаяЧто делает
hhelpСписок команд
nnextСледующая строка
sstepЗайти внутрь функции
ccontinueПродолжить до точки останова
rreturnДо конца текущей функции
b 42break 42Установить точку останова на строке 42
b file.py:42Точка останова в другом файле
clclearУдалить точку останова
p varprintПоказать значение
pp varpretty printКрасиво напечатать (для больших dict)
llistПоказать код
lllong listПоказать всю функцию
wwhereСтек вызовов
uupНа уровень выше в стеке
ddownНа уровень ниже
qquitВыйти
argsargsАргументы текущей функции
interactОткрыть REPL с текущим контекстом

interact особенно мощный: позволяет писать произвольный Python-код в контексте остановленной функции.

pdb++ и альтернативы

Pdb из коробки минималистичен. Альтернативы лучше:

  • pdb++ (или pdbpp) - drop-in замена с подсветкой, sticky-режимом, лучшим интерфейсом:
pip install pdbpp

Просто pip install - и стандартный breakpoint() начнёт открывать улучшенный отладчик.

  • ipdb - pdb с фичами ipython (автодополнение, цвета):
pip install ipdb

Чтобы breakpoint() использовал ipdb:

export PYTHONBREAKPOINT=ipdb.set_trace
  • Встроенный отладчик IDE - VS Code, PyCharm имеют GUI-отладчики, обычно удобнее командной строки для сложных сценариев.

Логирование вместо print

print() - тоже инструмент отладки, но в долгосрочной перспективе хуже логирования:

print(f"DEBUG: user={user}")  # быстро, но
                              # - нет уровней
                              # - не отключается
                              # - попадёт в продакшн вывод

Альтернатива:

import logging
logging.basicConfig(level=logging.DEBUG)

logging.debug("user=%s", user)

В продакшене можно поставить level=logging.INFO - и debug-сообщения молча отключатся. Подробнее в уроке про logging.

Полезный flow: исследуй в ipython, переноси в код

Типичный сценарий для backend-задачи:

  1. Открываешь ipython
  2. Импортируешь библиотеку, читаешь её документацию через ?
  3. Тестируешь API в REPL
  4. Когда понятно как работает - копируешь в файл

Это часто быстрее чем сразу писать код и потом дебажить.

Мини-задание

  1. Установи ipython в venv своего проекта:
pip install ipython
ipython

В REPL попробуй:

data = {"name": "Alice", "age": 30}
data.<TAB>          # увидь автодополнение
data.keys?           # документация
%timeit sum(range(1000))
  1. Создай debug_demo.py:
def divide(a, b):
    breakpoint()
    return a / b

result = divide(10, 0)
print(result)

Запусти python3 debug_demo.py. В pdb попробуй:

  • p a - что увидишь
  • p b - что увидишь
  • n - перейти на следующую строку (получишь ZeroDivisionError)
  • q - выйти
  1. Запусти скрипт через -i:
python3 -i script_with_function.py

Создай небольшой скрипт, и после его выполнения проверь переменные из скрипта в REPL.

Что дальше

Освоили инструменты для разработки. Модуль 1 завершён - у тебя есть Python, venv, понимание зависимостей и инструменты для экспериментов. В следующем модуле переходим к языку: типы данных, переменные, изменчивость и type hints.

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