Отладка циклов
Десять типовых ошибок циклов и универсальный метод их поиска — таблица трассировки, построенная вручную, строка за строкой.
Как читать чужой (и свой) сломанный цикл
Циклы — самое частое место, где программа работает, но выдаёт не тот результат, или вообще зависает. Хорошая новость: подавляющее большинство таких ошибок укладывается всего в десяток типовых сценариев. Разберём их по одному.
Ctrl+C в терминале, «Interrupt kernel» в Jupyter), не дожидаясь самостоятельной остановки.10 типичных ошибок циклов
while count < 5:
print(count)
# забыли count += 1
Условие никогда не станет ложным — бесконечный цикл. Проверьте, что внутри while есть строка, меняющая переменную из условия.
for i in range(1, 10):
print(i) # не выведет 10!
range(1, 10) даёт 1..9 — если нужно включить 10, это range(1, 11). Классическая off-by-one ошибка.
for n in chisla:
print(n) # SyntaxError: ожидался отступ
Тело цикла обязано иметь отступ. Без него Python даже не запустит программу.
for n in chisla:
kolichestvo = 0
kolichestvo += 1
kolichestvo сбрасывается на каждой итерации — в конце в нём всегда 1, а не общее количество. Инициализация должна быть ДО цикла, не внутри.
for n in chisla:
if n == 5:
break # не в теле if!
break, стоящий вне if (из-за отступа), сработает на первой же итерации независимо от условия. Проверяйте отступы так же внимательно, как и условия.
while count < 5:
if count == 2:
continue
print(count)
count += 1
Когда count == 2, continue пропускает count += 1 — и count навсегда останется равным 2. Бесконечный цикл, и его особенно трудно заметить.
for i in range(3):
for i in range(4):
print(i)
Внутренний цикл использует то же имя i, что и внешний — внешнее значение i затирается и теряется. Используйте разные имена, например i и j.
for n in range(10, 0):
print(n) # ничего не выведет
range(10, 0) с шагом по умолчанию +1 никогда не дойдёт от 10 до 0 — нужен range(10, 0, -1).
while spisok:
print(spisok[0])
# забыли удалить элемент из spisok
Условие проверяет spisok (пока список не пуст), но тело никогда не уменьшает список — бесконечный цикл, хотя выглядит иначе, чем счётчик.
n = 10
while n < 5:
print(n)
n += 1
Тело не выполнится ни разу — ноль итераций. Это не ошибка сама по себе, но частая неожиданность, если условие не проверили заранее.
Метод отладки: таблица трассировки вручную
Когда цикл ведёт себя не так, как ожидалось, самый надёжный способ разобраться — построить ту же таблицу трассировки, что мы использовали весь этот раздел, но теперь для реального, сломанного кода:
| Итерация | Условие | Переменные ДО тела | Что выводится | Переменные ПОСЛЕ тела |
|---|---|---|---|---|
| 1 | … | … | … | … |
| 2 | … | … | … | … |
| … | … | … | … | … |
Off-by-one — отдельный разбор
«Ошибка на единицу» — самая частая семья багов циклов: цикл выполняется на один раз больше или меньше, чем нужно.
Чек-лист граничных случаев
| Вопрос | Зачем проверять |
|---|---|
| Что произойдёт при нуле повторов (пустая последовательность)? | тело цикла не выполнится вообще — не сломается ли код после цикла |
| Что произойдёт при одном повторе? | самый частый источник ошибок на единицу |
| Включена ли последняя граница туда, куда нужно? | stop у range() и срезов не включён по умолчанию |
| Совпадает ли направление шага с направлением start→stop? | положительный шаг для роста, отрицательный — для убывания |
| Случай | Пример | Сколько итераций |
|---|---|---|
| Ноль | for n in []: | 0 — тело не выполнится ни разу |
| Один | for n in [7]: | 1 |
| Много | for n in range(1000): | 1000 |
Переменная цикла после его завершения
for n in range(5):
pass
print(n) # 4 — последнее значение, которое приняла n
for переменная цикла остаётся связана с последним полученным значением — в отличие от некоторых других языков, в Python у циклов нет отдельной «области видимости». Исключение: если последовательность была пустой, тело не выполнилось ни разу, и переменная цикла вообще не была создана — обращение к ней после цикла в этом случае вызовет NameError.Имена переменных цикла
Короткие имена вроде i, j,
k — давняя традиция для простых числовых счётчиков (особенно во
вложенных циклах), но как только переменная цикла имеет реальный смысл — называйте её
осмысленно: for slovo in slova: читается понятнее, чем
for x in y:.
Предпросмотр: изменение списка во время перебора
for элемент in spisok: может привести к тому, что некоторые элементы будут пропущены или обработаны дважды. Подробно разберём эту тему и безопасные способы её обхода в главе про списки — сейчас достаточно знать, что так делать не стоит.Немного о производительности
Каждая лишняя вложенность цикла умножает число итераций (мы уже видели это на примере таблицы умножения). Пока считать нужно тысячи, а не миллионы значений — разница незаметна. Формальный разбор скорости алгоритмов ждёт вас в старших главах — сейчас достаточно интуиции: вложенный цикл внутри цикла обычно работает заметно медленнее одного цикла того же размера.