Отладка B: процесс отладки и отладчик VS Code
- Применять пять шагов отладки — Reproduce, Isolate, Root Cause, Fix, Verify
- Ставить Breakpoint и запускать код в режиме Debug в VS Code
- Пользоваться Step Over, Step Into и Continue и следить за переменными в окне Variables
- Находить и исправлять Logical Error в настоящем коде с помощью отладчика
Что мы изучим?
В предыдущем уроке мы научились читать ошибки. Теперь научимся охотиться на баги, в том числе на те, у которых нет сообщения об ошибке:
- Процесс — пять фиксированных шагов для любого бага.
- Инструмент — отладчик (Debugger) VS Code: остановить код посередине и проверить, что происходит внутри.
- Настоящую практику — вместе решим баг шаг за шагом.
Зачем это нужно?
Путь новичков — «угадай и поменяй»: что-то поменять, запустить, а если не сработало — поменять что-то ещё. Это медленно, раздражает и часто создаёт новый баг. Профессионалы работают по методу, а отладчик позволяет увидеть код «в замедленной съёмке», вместо того чтобы гадать, что в нём происходит.
Объяснение с основ
Пять шагов отладки
| Шаг | Название | На какой вопрос отвечает |
|---|---|---|
| 1 | Reproduce (воспроизвести) | Как заставить баг происходить каждый раз? С каким вводом? |
| 2 | Isolate (изолировать) | Где в коде проблема? Сузить до строки или функции. |
| 3 | Root Cause (первопричина) | Почему это происходит? Не симптом, а причина. |
| 4 | Fix (исправить) | Исправить причину, а не скрыть симптом. |
| 5 | Verify (проверить) | Баг исчез? И ничего другого не сломалось? |
Хороший врач не даёт каждому пациенту обезболивающее. Он воспроизводит («когда болит?»), изолирует («где именно?»), находит причину (анализы), лечит причину и проверяет на повторном приёме, что пациент выздоровел. Одно обезболивающее — это «исправление симптома». Боль вернётся, а проблема останется.
Что такое отладчик?
Отладчик (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) | Остановить отладку. |
Вы читаете рецепт, и в нём строка «приготовить тесто (см. стр. 40)». Step Over = вы готовите тесто, не открывая страницу 40, и переходите к следующей строке рецепта. Step Into = вы открываете страницу 40 и читаете рецепт теста строка за строкой.
Синтаксис
Как запустить отладчик в VS Code:
- Убедитесь, что установлено расширение Python от Microsoft. В него входит отладчик.
- Откройте файл
.py. - Установка Breakpoint: щёлкните на левом поле, слева от номера строки. Появится красная точка 🔴 (или
F9на строке). - Запуск:
F5или маленькая стрелка рядом с кнопкой ▷ → Python Debugger: Debug Python File. Если спросят, какую конфигурацию, выберите Python File. - Программа останавливается на Breakpoint. Жёлтая строка — следующая строка, которая будет выполнена.
- Слева появляются 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)Упражнение:
- Поставьте Breakpoint на строку
total = total + apply_tax(price). F5. Программа останавливается. В окне Variables:price = 100,total = 0.F10(Step Over). Теперьtotal = 117.0, а жёлтая строка вернулась кfor.F5(Continue). Снова останавливаемся на той же строке:price = 250.- На этот раз
F11(Step Into). Мы внутриapply_tax, иamount = 250. В Call Stack видим:apply_tax→<module>. F10дважды: видимtax = 42.5, а затемreturn.F5до конца.
Вывод:
Total with tax: 456.3Разбор кода построчно
Что мы видим при каждой остановке — так мы понимаем ход выполнения:
| Остановка | Жёлтая строка | Variables | Объяснение |
|---|---|---|---|
| 1 | total = total + apply_tax(price) | price=100, total=0 | Остановка до выполнения строки, поэтому total ещё 0. |
После F10 | for price in prices: | total=117.0 | Строка выполнена полностью, включая функцию. |
| 2 | total = total + apply_tax(price) | price=250, total=117.0 | Второй круг. |
После F11 | tax = amount * 0.17 | amount=250 | Мы вошли в функцию. Видим только её переменные (Local). |
После F10 | return amount + tax | amount=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 строка за строкой:
| Круг | grade | grade < lowest? | lowest |
|---|---|---|---|
| Начало | — | — | 0 |
| 1 | 80 | 80 < 0 → False | 0 |
| 2 | 90 | 90 < 0 → False | 0 |
| 3 | 70 | 70 < 0 → False | 0 |
| 4 | 100 | 100 < 0 → False | 0 |
Первопричина: 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
Показать решение
- Reproduce — снова вызвать баг, намеренно, с известным вводом.
- Isolate — сузить: в какой функции, на какой строке и с каким значением начинается проблема.
- Understand — понять, почему это происходит, прежде чем менять код.
- Fix — исправить первопричину, а не симптом.
- 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 в списке вообще нет.
- Куда бы вы поставили Breakpoint и что увидели бы в окне Variables?
- Исправьте баг.
Ожидаемый вывод:
9
-3Показать решение
- Breakpoint на строке
if number > largest:. В окне Variables видно, чтоlargestостаётся 0 на протяжении всего цикла — ни одно отрицательное число не больше 0. - Начинать с первого элемента списка, а не с 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: Инструменты отладчика
Объясните каждый термин одним предложением и когда вы им пользуетесь:
- Breakpoint
- Step Over
- Step Into
- Окно Variables
Показать решение
- Breakpoint — точка, где выполнение останавливается, чтобы можно было проверить состояние в этот момент. Ставьте его на строку перед подозрительным местом.
- Step Over — выполняет текущую строку и переходит к следующей, не заходя в вызываемые в ней функции.
- Step Into — заходит в функцию, вызываемую в текущей строке, чтобы проверить, что происходит внутри.
- Окно 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 находят, сравнивая фактические значения переменных с ожидаемыми, строка за строкой.
- После исправления проверьте и граничные случаи: пусто, один элемент, ноль, отрицательные.