# Отладка B: процесс отладки и отладчик VS Code<no value>

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

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

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

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

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

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

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

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

{{< box "analogy" "Аналогия: врач" >}}
Хороший врач не даёт каждому пациенту обезболивающее. Он **воспроизводит** («когда болит?»), **изолирует** («где именно?»), находит **причину** (анализы), **лечит** причину и **проверяет** на повторном приёме, что пациент выздоровел. Одно обезболивающее — это «исправление симптома». Боль вернётся, а проблема останется.
{{< /box >}}

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

**Отладчик** (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`) | Остановить отладку. |

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

## Синтаксис

**Как запустить отладчик в 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`:

```python
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` до конца.

**Вывод:**

```text
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`:

```python
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**.

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

```text
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:** начинать с **первой** оценки в списке. Она всегда настоящий кандидат:

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

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

**Шаг 5 — Verify:**

```text
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): пустой список, один элемент, отрицательные значения, ноль.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

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

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

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

Fix · Reproduce · Verify · Understand · Isolate

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

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

```python
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. Исправьте баг.

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

```text
9
-3
```

{{< solution >}}
1. Breakpoint на строке `if number > largest:`. В окне Variables видно, что `largest` остаётся **0** на протяжении всего цикла — ни одно отрицательное число не больше 0.
2. Начинать с первого элемента списка, а не с 0:

```python
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]))
```

```text
9
-3
```
{{< /solution >}}

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

```python
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 проследите за циклом — что происходит после первой буквы? Исправьте.

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

```text
3
```

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

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


print(count_vowels("banana"))
```

```text
3
```
{{< /solution >}}

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

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

1. Breakpoint
2. Step Over
3. Step Into
4. Окно Variables

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

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

```python
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. Найдите баг и исправьте его.

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

```text
15
```

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

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


print(sum_to(5))
```

```text
15
```
{{< /solution >}}

<!-- exercises:end -->

## Итоги

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

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

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

- [VS Code — Python debugging](https://code.visualstudio.com/docs/python/debugging)
- [VS Code — Debugging: Breakpoints](https://code.visualstudio.com/docs/debugtest/debugging#_breakpoints)

<!-- docs:end -->
