Запрещено менять код без гипотезы: правка наугад — лотерея.
На каком шаге находится тот, кто говорит „у меня работает"? (шаг 1 не пройден — нет воспроизведения)
«Шаг 1 — воспроизвести: минимальный стабильный пример, а не "иногда бывает"»
defavg(nums):
returnsum(nums) / len(nums)
print(avg([1, 2, 3]))
print(avg([]))
2.0Traceback (most recent call last):
File "debug.py", line 4, in <module>
print(avg([]))
File "debug.py", line 2, in avg
return sum(nums) / len(nums)
ZeroDivisionError: division by zero
Правило: «баг появляется иногда» — это отсутствие воспроизведения; воспроизвести = получить ошибку на минимальном стабильном примере (здесь — avg([])).
Почему «у меня работает» — это не воспроизведение? (нет данных, на которых баг проявляется)
«Шаг 2–3 — гипотеза и эксперимент: проверяем предположение, а не код»
# Гипотеза: avg падает на пустом списке, потому что len(nums) == 0.# Эксперимент: проверить предположение, добавив обработку пустого списка.defavg(nums):
returnsum(nums) / len(nums) if nums else 0.0
print(avg([1, 2, 3]))
print(avg([]))
2.0
0.0
Правило: эксперимент подтвердил гипотезу (пустой список — причина); фикс следует из гипотезы, а не наоборот.
Что было бы, если бы гипотеза опроверглась? (новая гипотеза — стрелка назад на схеме)
# антипример: «исправление» наугад — баг становится «плавающим»deffirst_n(n):
returnlist(range(n + 1)) # изменили без гипотезыprint(first_n(5)) # [0, 1, 2, 3, 4, 5] — стало хужеprint(first_n(0)) # [0] — а для 0 теперь вообще неверно[0, 1, 2, 3, 4, 5]
[0]
Правило: правка без гипотезы не проверяет причину — она сдвигает ошибку («баг плавает»); метод требует гипотезы до правки.
Что произошло с first_n(0)? (было [] — а стало [0]). Как бы вы исправили правильно?
«Стоп-вопрос: почему первый шаг — воспроизведение, а не исправление?»
Почему первый шаг отладки — воспроизведение, а не исправление?
Что вы узнаёте из эксперимента, который опроверг гипотезу?
Эталон: без воспроизведения нет данных, на которых проверять фикс; опровергнутая гипотеза — это новая информация (причина не там) и новая гипотеза.
Пауза 10–15 секунд; затем — эталон и переход к чтению traceback.
«Traceback — это цепочка: тип, сообщение, файл, строка, вызовы»
# debug.pydefload(path):
returnopen(path).read()
defmain():
data = load("missing.txt")
print(data)
main()
Traceback (most recent call last):
File "/tmp/.../debug.py", line 8, in <module>
main()
File "/tmp/.../debug.py", line 5, in main
data = load("missing.txt")
File "/tmp/.../debug.py", line 2, in load
return open(path).read()
FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
Правило: не читать только последнюю строку — полный стек показывает цепочку вызовов от места ошибки вверх к точке входа.
Что именно сообщает последняя строка? (тип исключения + точную причину в сообщении)
«Элементы traceback: что искать в каждой части»
Часть traceback
Что искать
последняя строка
тип исключения + точную причину в сообщении
блоки File "...", line N
где именно возникло, какой вызов привёл
строка с ^ (Python 3.11)
точное место в строке
цепочка блоков
путь от места ошибки к точке входа
raise ... from
причинную связь между исключениями
Правило: сначала последняя строка (тип + причина), затем цепочка вызовов (какой вызов привёл).
Зачем читать блоки вызовов, если причина — в последней строке? (узнать, КАКОЙ вызов передал неверные данные)
«Порядок чтения: снизу вверх по стеку — где возникло, потом — что привело»
Читаем снизу вверх: где реально возникло, потом — какой вызов привёл; верхний блок — точка входа.
Ошибка возникла в load, а привёл её вызов из main — кто передал плохой путь?
«Причинная цепочка raise ... from — диагностический инструмент»
try:
int("abc")
exceptValueErroras e:
raiseRuntimeError("не удалось распарсить") from e
Traceback (most recent call last):
File "chain.py", line 2, in <module>
int("abc")
ValueError: invalid literal for int() with base 10: 'abc'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "chain.py", line 4, in <module>
raise RuntimeError("не удалось распарсить") from e
RuntimeError: не удалось распарсить
Правило:from e сохраняет первопричину — traceback показывает оба исключения; без from e цепочка потерялась бы.
Что показывает фраза «The above exception was the direct cause»? (первопричину)
«Live-coding: breakpoint() вживую — n, s, c, p, l, q»
Задача: функция total(rows) суммирует расходы по категориям из списка словарей; перед вызовом стоит breakpoint(). Пройдите сессию отладчика — код преподаватель пишет с нуля.
l — листинг вокруг текущей строки;
n — next: следующая строка, не входя в функции;
p rows — напечатать список rows;
s — step: войти в total;
p result — напечатать result на первом витке цикла;
c — continue → вывод {'food': 1500, 'fun': 2000}.
Намеренно показать 1 ошибку: p r до входа в цикл → NameError — разобрать: переменная ещё не создана.
После блока — 1–2 минуты: поставьте breakpoint() в свою функцию и пройдите n/p.
«print-дебаг vs breakpoint(): когда что использовать»
Критерий
print-дебаг
breakpoint()/pdb
Скорость догадки
быстрый
чуть медленнее (остановка)
Правка кода
требует вставки и удаления строк
не требует правки логики
Состояние в момент ошибки
не показывает (только после print)
показывает в момент останова
Где применим
одноразовые скрипты, быстрые проверки
сложные цепочки вызовов, циклы
Правило:print годится для быстрых догадок; breakpoint() — когда нужно состояние в момент ошибки; print, оставленный в коде, — замусоривание вывода.
Когда print-дебаг ещё допустим? (одноразовый скрипт)
«Команды pdb: n next, s step, c continue, p print, l list, q quit»
> pdb_demo.py(10)<module>()
-> print(total(rows))
(Pdb) l # листинг вокруг текущей строки
(Pdb) n # next: следующая строка, не входя в функции
(Pdb) s # step: войти в функцию
(Pdb) p rows # напечатать выражение[{'category': 'food', 'sum': 1500}, {'category': 'fun', 'sum': 2000}]
(Pdb) c # continue: до следующей точки останова
(Pdb) q # quit: выйти из отладчика
n — следующая строка, не входя в функции;
s — войти в функцию;
c — continue: до следующей точки останова;
p <expr> — напечатать выражение;
l — листинг вокруг текущей строки;
q — quit: выйти из отладчика.
Команды повторяются нажатием Enter — с какой команды вы начнёте на семинаре?
«Логирование вместо print: logging с уровнями DEBUG/INFO/WARNING/ERROR»
import logging
logging.basicConfig(level=logging.INFO)
logging.debug("детали отладки")
logging.info("старт работы")
logging.warning("внимание: файл не найден")
logging.error("ошибка: не удалось прочитать")
INFO:root:старт работы
WARNING:root:внимание: файл не найден
ERROR:root:ошибка: не удалось прочитать
Правило: уровни DEBUG/INFO/WARNING/ERROR; debug не выводится при уровне INFO — выключение без правки кода; можно писать в файл и консоль; print допустим только в одноразовом скрипте.
Почему print-дебаг в CLI-утилите — плохо? (замусоривает вывод, выключить нельзя без правки кода)
«Стоп-вопрос: в чём разница между print-дебагом и breakpoint()?»
В чём разница между print-дебагом и breakpoint()? Когда что использовать?
Что вы узнаёте из полного traceback, если читаете его целиком, а не только последнюю строку?
Эталон: print — быстро, но требует правки кода и не показывает состояние в момент ошибки; breakpoint — остановка с полным состоянием; полный traceback — цепочку вызовов и первопричину.
Пауза 10–15 секунд; затем — переход к тестам как диагностике.
«Тест как диагностика: тест-на-баг, который падает ДО фикса»
Состояние ДО фикса ✓: тест падает — add_transaction принимает отрицательную сумму без ошибки, pytest.raises(ValueError) не срабатывает.
Правило: написать тест, воспроизводящий баг → убедиться, что он падает (это и есть воспроизведение) → починить → тест зелёный.
Почему тест должен падать ДО фикса? (иначе неизвестно, что он ловит именно этот баг)
«Фикс: тест, падавший на баге, — зелёный; фикс без теста — не доказан»
defadd_transaction(transactions, item):
if item["sum"] < 0:
raiseValueError("сумма не может быть отрицательной")
transactions.append(item)
return transactions
Состояние ПОСЛЕ фикса ✓: test_negative_amount_raises — зелёный (pytest.raises(ValueError) срабатывает).
Правило: тест, который не падал на баге, бесполезен; фикс без теста — не доказан («исправил, вроде работает» — через месяц баг вернулся).
Что доказывает зелёный тест после фикса? (баг воспроизводился и больше не воспроизводится)
«Тест не падал на баге — бесполезен; тест падал — это и есть воспроизведение»
Тест
Статус
Что означает
падал на баге, зелёный после фикса
полезный
баг воспроизведён и устранён, регрессия поймана
не падал на баге (писался «на всякий случай»)
бесполезный
неизвестно, ловит ли тест этот баг
фикс без теста
не доказан
«исправил, вроде работает» — баг может вернуться
Правило-вывод: тест-на-баг — это диагностика в форме проверки; «красный → зелёный» — интуитивный задел на TDD.
Как проверить, что тест полезен? (удалить фикс — тест должен упасть)
«Задел на TDD: полный цикл red-green-refactor — модуль 5 трека»
Сегодня: тест пишется ПОСЛЕ обнаружения бага и фиксирует его.
TDD (модуль 5 трека): тест пишется ДО кода — как спецификация; полный цикл red-green-refactor.
Общее: тест — диагностика; зелёная полоса — критерий «сделано».
Что общего между тестом-на-баг и TDD? (тест первичен, зелёный — критерий)
«Так нельзя: правка наугад в "проде" — без минимального примера»
# антипример: код с багомdeftotal_by_category(rows, limit):
result = {}
for r in rows:
if r["sum"] > limit:
cat = r["category"]
result[cat] = result.get(cat, 0) + r["sum"]
return result
«Как не надо»: правка наугад — меняем > на >= без гипотезы; на одних данных «прошло», на других (граница limit) — поведение изменилось, и баг «плавает».
Воспроизвести: взять баг из Слайда 3 (first_n) — нужен конкретный баг, а не «иногда бывает».
Гипотеза: off-by-one в range(n - 1).
Эксперимент: range(n) на n = 5 и n = 0.
Фикс: return list(range(n)).
Тест зелёный — баг воспроизведён и устранён.
Намеренная ошибка недели: правка наугад без гипотезы и без минимального примера. После блока — 1 минута: «напишите тест-на-баг для first_n(5)».
Правка наугад — лотерея: вы починили один вход и сломали другой. Чем полный цикл метода отличается от неё?
«Шесть ошибок недели 14, которые вы совершите на семинаре»
правка «наугад» без гипотезы — код меняется, баг «плавает»;
игнорирование сообщения об ошибке — читают только «Traceback» и ищут «где-то рядом», хотя сообщение называет точную причину;
отладка «в проде» — правят боевой код, не сделав минимальный воспроизводимый пример;
отсутствие воспроизводимого примера — «баг появляется иногда», метод не работает;
фикс без теста — «исправил, вроде работает», через месяц баг вернулся;
print-дебаг оставлен в коде — замусоривают вывод утилиты.
Какая ошибка дороже всего в эксплуатации сервиса? (№ 5 — фикс без теста: регрессия возвращается)
«Проверь себя: 4 вопроса перед семинаром»
Почему первый шаг отладки — воспроизведение, а не исправление?
Что вы узнаёте из полного traceback, если читаете его целиком, а не только последнюю строку?
В чём разница между print-дебагом и breakpoint()? Когда что использовать?
Почему тест, который не падал на баге, бесполезен?
2 минуты письменно, затем короткий разбор (эталоны — в решения-14.md).