Отладка B: процесс отладки и отладчик VS Code

Цели урока
  • Применять пять шагов отладки — Reproduce, Isolate, Root Cause, Fix, Verify
  • Ставить Breakpoint и запускать код в режиме Debug в VS Code
  • Пользоваться Step Over, Step Into и Continue и следить за переменными в окне Variables
  • Находить и исправлять Logical Error в настоящем коде с помощью отладчика

Что мы изучим?

В предыдущем уроке мы научились читать ошибки. Теперь научимся охотиться на баги, в том числе на те, у которых нет сообщения об ошибке:

  1. Процесс — пять фиксированных шагов для любого бага.
  2. Инструмент — отладчик (Debugger) VS Code: остановить код посередине и проверить, что происходит внутри.
  3. Настоящую практику — вместе решим баг шаг за шагом.

Зачем это нужно?

Путь новичков — «угадай и поменяй»: что-то поменять, запустить, а если не сработало — поменять что-то ещё. Это медленно, раздражает и часто создаёт новый баг. Профессионалы работают по методу, а отладчик позволяет увидеть код «в замедленной съёмке», вместо того чтобы гадать, что в нём происходит.

Объяснение с основ

Пять шагов отладки

ШагНазваниеНа какой вопрос отвечает
1Reproduce (воспроизвести)Как заставить баг происходить каждый раз? С каким вводом?
2Isolate (изолировать)Где в коде проблема? Сузить до строки или функции.
3Root Cause (первопричина)Почему это происходит? Не симптом, а причина.
4Fix (исправить)Исправить причину, а не скрыть симптом.
5Verify (проверить)Баг исчез? И ничего другого не сломалось?
Аналогия: врач

Хороший врач не даёт каждому пациенту обезболивающее. Он воспроизводит («когда болит?»), изолирует («где именно?»), находит причину (анализы), лечит причину и проверяет на повторном приёме, что пациент выздоровел. Одно обезболивающее — это «исправление симптома». Боль вернётся, а проблема останется.

Что такое отладчик?

Отладчик (Debugger) — инструмент, который запускает программу под вашим контролем: он останавливает её на выбранной строке и позволяет продвигаться строка за строкой и видеть значение каждой переменной в каждый момент.

Термины:

ТерминЧто это
Breakpoint«Точка остановки»: отметка на строке. Когда выполнение доходит до неё, программа останавливается до выполнения строки.
Continue (F5)Продолжить обычное выполнение до следующего Breakpoint (или до конца).
Step Over (F10)Выполнить текущую строку полностью и остановиться на следующей. Если в ней вызов функции, функция выполняется «целиком».
Step Into (F11)Если в строке вызов функции, войти в неё и остановиться на её первой строке.
Step Out (Shift+F11)Завершить текущую функцию и вернуться к тому, кто её вызвал.
VariablesОкно, которое показывает все переменные и их значения в данный момент.
WatchВыражения, за которыми вы решили следить, например total / count.
Call StackЦепочка вызовов: какая функция вызвала какую. Это Traceback, только «живой».
Stop (Shift+F5)Остановить отладку.
Step Over и Step Into

Вы читаете рецепт, и в нём строка «приготовить тесто (см. стр. 40)». Step Over = вы готовите тесто, не открывая страницу 40, и переходите к следующей строке рецепта. Step Into = вы открываете страницу 40 и читаете рецепт теста строка за строкой.

Синтаксис

Как запустить отладчик в VS Code:

  1. Убедитесь, что установлено расширение Python от Microsoft. В него входит отладчик.
  2. Откройте файл .py.
  3. Установка Breakpoint: щёлкните на левом поле, слева от номера строки. Появится красная точка 🔴 (или F9 на строке).
  4. Запуск: F5 или маленькая стрелка рядом с кнопкой ▷ → Python Debugger: Debug Python File. Если спросят, какую конфигурацию, выберите Python File.
  5. Программа останавливается на Breakpoint. Жёлтая строка — следующая строка, которая будет выполнена.
  6. Слева появляются Variables, Watch и Call Stack. Вверху появляется панель кнопок: Continue, Step Over, Step Into, Step Out, Restart, Stop.

Трюк: во время остановки можно навести мышь на переменную в коде и увидеть её значение.

Первый пример кода

Начнём с правильной программы, просто чтобы познакомиться с инструментом. week-2/debug_intro.py:

def apply_tax(amount):
    tax = amount * 0.17
    return amount + tax

prices = [100, 250, 40]
total = 0

for price in prices:
    total = total + apply_tax(price)

print("Total with tax:", total)

Упражнение:

  1. Поставьте Breakpoint на строку total = total + apply_tax(price).
  2. F5. Программа останавливается. В окне Variables: price = 100, total = 0.
  3. F10 (Step Over). Теперь total = 117.0, а жёлтая строка вернулась к for.
  4. F5 (Continue). Снова останавливаемся на той же строке: price = 250.
  5. На этот раз F11 (Step Into). Мы внутри apply_tax, и amount = 250. В Call Stack видим: apply_tax → <module>.
  6. F10 дважды: видим tax = 42.5, а затем return.
  7. F5 до конца.

Вывод:

Total with tax: 456.3

Разбор кода построчно

Что мы видим при каждой остановке — так мы понимаем ход выполнения:

ОстановкаЖёлтая строкаVariablesОбъяснение
1total = total + apply_tax(price)price=100, total=0Остановка до выполнения строки, поэтому total ещё 0.
После F10for price in prices:total=117.0Строка выполнена полностью, включая функцию.
2total = total + apply_tax(price)price=250, total=117.0Второй круг.
После F11tax = amount * 0.17amount=250Мы вошли в функцию. Видим только её переменные (Local).
После F10return amount + taxamount=250, tax=42.5

Ещё один пример

Настоящий баг — от начала до конца. Программа, которая вычисляет средний балл без самой низкой оценки (как принято на курсах: «самая низкая оценка не учитывается»). week-2/drop_lowest.py:

def find_lowest(grades):
    lowest = 0
    for grade in grades:
        if grade < lowest:
            lowest = grade
    return lowest

def average_without_lowest(grades):
    lowest = find_lowest(grades)
    total = sum(grades) - lowest
    return total / (len(grades) - 1)

my_grades = [80, 90, 70, 100]
print("Average without lowest:", average_without_lowest(my_grades))

Ожидание: самая низкая — 70. Остаются 80, 90, 100, и среднее — 90.0.

Фактический вывод:

Average without lowest: 113.33333333333333

Среднее выше 100! Это Logical Error: никакого сообщения об ошибке, просто невозможный результат.

Шаг 1 — Reproduce: ввод [80, 90, 70, 100] вызывает баг при каждом запуске. Отлично, у нас стабильный случай.

Шаг 2 — Isolate: функций две. Какая виновата? Поставьте Breakpoint на строку total = sum(grades) - lowest и запустите через F5. В окне Variables: lowest = 0. Но самая низкая — 70! Проблема уже до этой строки, внутри find_lowest. Мы её изолировали.

Шаг 3 — Root Cause: остановите, поставьте Breakpoint на lowest = find_lowest(grades), F5, затем F11 (Step Into) внутрь find_lowest и F10 строка за строкой:

Кругgradegrade < lowest?lowest
Начало——0
18080 < 0 → False0
29090 < 0 → False0
37070 < 0 → False0
4100100 < 0 → False0

Первопричина: lowest начинается с 0. Ни одна положительная оценка не меньше 0, поэтому значение никогда не обновляется. Предположение «0 — хорошее начальное значение» неверно.

Шаг 4 — Fix: начинать с первой оценки в списке. Она всегда настоящий кандидат:

def find_lowest(grades):
    lowest = grades[0]
    for grade in grades:
        if grade < lowest:
            lowest = grade
    return lowest

(В Python есть и встроенная min(grades), и в настоящем коде лучше использовать её. Здесь мы написали её вручную для практики.)

Шаг 5 — Verify:

Average without lowest: 90.0

И проверьте другие случаи, не только исходный:

  • [50, 50, 50] → 50.0 ✓
  • [100, 0] → 100.0 ✓ (самая низкая, 0, удаляется)
  • [70] → падает! ZeroDivisionError (деление на len - 1 = 0). Мы нашли ещё один баг благодаря шагу проверки. Что делать со списком из одной оценки? Это бизнес-решение. Например: вернуть саму оценку.

Типичная ошибка

Ошибка 1: пропустить Reproduce. «Пользователь сказал, что иногда не работает», — и вы меняете код без случая, который воспроизводит баг. Без воспроизведения невозможно узнать, что вы его исправили.

Ошибка 2: отлаживать только через print и не убирать за собой. Временные печати (print("HERE", x)) — законный и быстрый инструмент, но их забывают в коде, и они засоряют вывод. Отладчик показывает все переменные, не меняя код. (А в теме исключений и логирования мы изучим логирование (Logging) — профессиональный способ оставлять информацию в коде.)

Ошибка 3: Step Into во всё. F11 на print(...) или sum(...) уводит во внутренний код Python, и там легко заблудиться. Используйте Step Into для своих функций, а Step Over — для встроенных. (Если вошли по ошибке, Shift + F11 выведет вас обратно.)

Ошибка 4: исправить и не проверить другие случаи. Как мы видели, шаг Verify выявил второй баг. Всегда проверяйте и граничные случаи (edge cases): пустой список, один элемент, отрицательные значения, ноль.

Практика

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

Упражнение 1: Пять шагов

Расставьте шаги отладки по порядку и объясните одним предложением, что вы делаете на каждом шаге:

Fix · Reproduce · Verify · Understand · Isolate

Показать решение
  1. Reproduce — снова вызвать баг, намеренно, с известным вводом.
  2. Isolate — сузить: в какой функции, на какой строке и с каким значением начинается проблема.
  3. Understand — понять, почему это происходит, прежде чем менять код.
  4. Fix — исправить первопричину, а не симптом.
  5. Verify — снова запустить случай, который падал, а также случаи, которые работали раньше.

Упражнение 2: Максимум, который получился 0

def find_max(numbers):
    largest = 0
    for number in numbers:
        if number > largest:
            largest = number
    return largest

print(find_max([4, 9, 2]))
print(find_max([-7, -3, -10]))

Второй вызов возвращает 0, хотя 0 в списке вообще нет.

  1. Куда бы вы поставили Breakpoint и что увидели бы в окне Variables?
  2. Исправьте баг.

Ожидаемый вывод:

9
-3
Показать решение
  1. Breakpoint на строке if number > largest:. В окне Variables видно, что largest остаётся 0 на протяжении всего цикла — ни одно отрицательное число не больше 0.
  2. Начинать с первого элемента списка, а не с 0:
def find_max(numbers):
    largest = numbers[0]
    for number in numbers:
        if number > largest:
            largest = number
    return largest


print(find_max([4, 9, 2]))
print(find_max([-7, -3, -10]))
9
-3

Упражнение 3: return внутри цикла

def count_vowels(word):
    count = 0
    for letter in word:
        if letter in "aeiou":
            count = count + 1
        return count

print(count_vowels("banana"))

Функция возвращает 0 вместо 3. С помощью Step Over проследите за циклом — что происходит после первой буквы? Исправьте.

Ожидаемый вывод:

3
Показать решение

С помощью Step Over видно, что после первой буквы (b, не гласная) выполнение сразу идёт к return count — внутри цикла, — и функция завершается с 0. return смещён на один уровень отступа слишком глубоко.

def count_vowels(word):
    count = 0
    for letter in word:
        if letter in "aeiou":
            count = count + 1
    return count


print(count_vowels("banana"))
3

Упражнение 4: Инструменты отладчика

Объясните каждый термин одним предложением и когда вы им пользуетесь:

  1. Breakpoint
  2. Step Over
  3. Step Into
  4. Окно Variables
Показать решение
  1. Breakpoint — точка, где выполнение останавливается, чтобы можно было проверить состояние в этот момент. Ставьте его на строку перед подозрительным местом.
  2. Step Over — выполняет текущую строку и переходит к следующей, не заходя в вызываемые в ней функции.
  3. Step Into — заходит в функцию, вызываемую в текущей строке, чтобы проверить, что происходит внутри.
  4. Окно Variables — показывает значения всех переменных в текущий момент. Именно здесь видно, где значение отличается от ожидаемого.

Упражнение 5: Ошибка на единицу (off-by-one)

def sum_to(n):
    total = 0
    for i in range(1, n):
        total = total + i
    return total

print(sum_to(5))

Сумма 1+2+3+4+5 должна быть 15, а получается 10. Найдите баг и исправьте его.

Ожидаемый вывод:

15
Показать решение

range(1, n) не включает n — цикл проходит только по 1, 2, 3, 4. Это классический баг «Off-by-one» (ошибка на единицу).

def sum_to(n):
    total = 0
    for i in range(1, n + 1):
        total = total + i
    return total


print(sum_to(5))
15

Итоги

  • Пять шагов: Reproduce → Isolate → Root Cause → Fix → Verify. Без угадывания.
  • Breakpoint останавливает до выполнения строки. F5 = Continue, F10 = Step Over, F11 = Step Into, Shift+F11 = Step Out.
  • Окно Variables показывает состояние программы в момент остановки. Call Stack показывает, кто кого вызвал.
  • Logical Error находят, сравнивая фактические значения переменных с ожидаемыми, строка за строкой.
  • После исправления проверьте и граничные случаи: пусто, один элемент, ноль, отрицательные.

Официальная документация