Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 14 из 20 Начальный 15 мин 100 XP

Ошибки в циклах Python: от отступа до зависания

Ошибка в цикле повторяется на каждой итерации. Три полки цикловых ошибок — падение, молчаливая логика, зависание — и быстрый способ найти каждую.

Редакция Питоники

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

IndentationError: отступ — часть синтаксиса

тело цикла без отступа
for i in range(3):
print(i)
Интерпретатор ответит IndentationError: expected an indented block after 'for' statement — тело цикла обязано быть с отступом.

Тело цикла без отступа — падение на старте. Python читает отступы как структуру программы: четыре пробела после двоеточия означают «это тело». Забыли — интерпретатор честно останавливается и показывает строку. Отступ и двоеточие — два знака, из-за которых падает половина первых циклов; полный разбор — в первом уроке раздела.

Отступ есть, но не тот: строка вне цикла

строка выскользнула из цикла
for i in range(3):
    print('внутри:', i)
print('снаружи:', i)
Вывод
внутри: 0
внутри: 1
внутри: 2
снаружи: 2

Ошибки нет — а замысел сломан. Строка снаружи: 2 выполнена один раз после цикла: переменная i пережила перебор и хранит последнее значение. Строка с отступом тела — внутри цикла; строка на уровне for — уже после него. Один уровень отступа меняет судьбу строки.

Off-by-one: на один шаг мимо

Классика молчаливых ошибок: цикл делает на шаг меньше или больше. Напечатаем дни недели с номерами — с первого взгляда всё правильно:

потерянный понедельник
days = ['пн', 'вт', 'ср', 'чт', 'пт']
for i in range(1, len(days)):
    print(i, days[i])
Вывод
1 вт
2 ср
3 чт
4 пт

Понедельник потерян: range(1, len(days)) начал с индекса 1, а индекс 0 — это и есть пн. Ошибка off-by-one не падает — она молча пропускает данные. Ловится просто: прогоните цикл на маленьком списке из трёх элементов — промах виден мгновенно.

лечение: enumerate
days = ['пн', 'вт', 'ср', 'чт', 'пт']
for i, day in enumerate(days):
    print(i, day)
Вывод
0 пн
1 вт
2 ср
3 чт
4 пт

enumerate сам раздаёт номера без ручных границ range — ошибиться в них невозможно. Если индексы всё же нужны руками, держите в голове правило range: правая граница не включается, поэтому range(len(days)) даёт ровно все индексы списка.

Изменение списка во время перебора

Самая коварная логическая ошибка: удалять элементы из того списка, по которому идёт for. Отсеем из очереди двух человек:

remove прямо в переборе
queue = ['Аня', 'Борис', 'Вера', 'Глеб']
for person in queue:
    if person == 'Борис' or person == 'Вера':
        queue.remove(person)
print(queue)
Вывод
['Аня', 'Вера', 'Глеб']

Вера должна была уйти — но осталась. for держится за номера позиций: после удаления Бориса список сдвинулся, Вера встала на его место, а курсор перебора шагнул дальше — прямо мимо неё. Python не ругается: формально всё законно, вот почему ошибка так долго живёт в коде.

лечение: собирать новый список
queue = ['Аня', 'Борис', 'Вера', 'Глеб']
stay = []
for person in queue:
    if person == 'Борис' or person == 'Вера':
        continue
    stay.append(person)
print(stay)
Вывод
['Аня', 'Глеб']

Лечение — не трогать перебираемый список: решения складываем в новый. continue отсеивает уходящих, stay собирает оставшихся — исходный список цел, никто не пропущен. Тот же фильтр одной строкой — генератор списка: stay = [p for p in queue if p not in ('Борис', 'Вера')] — двенадцатый урок о нём.

Зависание: условие, которое не меняется

сравнение вместо присваивания
battery = 40
while battery > 0:
    print('Разряд:', battery)
    battery == battery - 20  # двойное равно ничего не меняет
Блок не запускается: двойное равно лишь сравнивает и выбрасывает результат, battery вечно 40, условие вечно истинно — цикл завис. Присваивание пишется одним знаком: battery = battery - 20.

Тихая разновидность вечного цикла: код выглядит рабочим, отступы верны, печать идёт — и не останавливается. Причина — условие выхода зависит от переменной, которая не меняется: забыли шаг, поменяли == и =, уменьшается не та переменная. Страховка та же, что в уроке про while True: лимит итераций и печатный контроль; правила while с условием — в пятом уроке.

Как отлаживать цикл за минуту

Первый приём — печатный контроль: печать ключевых переменных внутри каждой итерации показывает, где счёт пошёл не так.

отладочная печать
prices = [89, 42, 590]
total = 0
for p in prices:
    total += p
    print('p =', p, '| total =', total)
print('Итог:', total)
Вывод
p = 89 | total = 89
p = 42 | total = 131
p = 590 | total = 721
Итог: 721

Видно всё: какой элемент пришёл и что стало с total после него. Логическая ошибка живёт на конкретной итерации — печать её подсвечивает. Второй приём — уменьшить данные: список из трёх элементов проверяется глазами. Третий — проверить края: первый и последний шаги цикла почти всегда и есть место off-by-one.

Ошибки в циклах — не случайность, а этап, который проходят все. Разница между новичком и опытным разработчиком не в числе ошибок, а в скорости поиска: печатный контроль, маленькие данные, глаза на границах — и цикл снова честный.

IndexError: выход за границу списка

От off-by-one в границах range есть ещё один родственник — падение с IndexError. Оно выглядит как обвинение Python, но на самом деле виновата арифметика: цикл зашёл на индекс, которого в списке нет.

индекс на шаг дальше последнего
marks = [4, 5, 3]

for i in range(1, len(marks) + 1):
    print(marks[i])
Не запускается: IndexError: list index out of range. Цикл идёт по индексам 1, 2, 3 — а последний элемент списка лежит под индексом 2. Граница на единицу больше нужной поймана и в начале, и в конце.
лечение: границы по длине списка
marks = [4, 5, 3]

for i in range(len(marks)):
    print(i, marks[i])
Вывод
0 4
1 5
2 3

len(marks) даёт тройку, и range(3) даёт индексы 0, 1, 2 — ровно столько, сколько элементов в списке. Тот же список через enumerate даст то же самое, но без ручной арифметики: enumerate сам раздаёт правильные номера. Если в коде встречается выражение len(...) рядом с числом, остановитесь и подумайте: правильно ли оно на месте — в девяти случаях из десяти ошибка именно там.

RuntimeError: словарь изменился во время перебора

Со списком Python тихо прощает правку во время перебора — элемент просто пропускается. Со словарём он не прощает: меняется размер, и интерпретатор падает с RuntimeError. Ошибка громкая, а значит — безопасная: заметили сразу.

удаление из словаря в цикле
prices = {'хлеб': 50, 'сыр': 300, 'молоко': 80}

for name in prices:
    if prices[name] > 100:
        del prices[name]
Не запускается: RuntimeError: dictionary changed size during iteration. Удаление ключа меняет размер словаря, а перебор по словарю идёт по его внутренней таблице — интерпретатор это пресекает.
лечение: собираем новый словарь
prices = {'хлеб': 50, 'сыр': 300, 'молоко': 80}

cheap = {}
for name, price in prices.items():
    if price <= 100:
        cheap[name] = price
print(cheap)
Вывод
{'хлеб': 50, 'молоко': 80}

Тот же приём, что со списком: не трогаем перебираемую структуру, решаем копить в новую. Ключи в новом словаре стоят в порядке первого появления, поэтому вывод предсказуем — и это ещё один повод не полагаться на порядок ключей при переборе исходного словаря.

Тихая ошибка: цикл не выполнился ни разу

Самая коварная полка — та, где цикл просто не начался. Никакой ошибки, никакого зависания: программа отработала и выдала пустоту, а человек ищет проблему в данных.

цикл по пустому списку
orders = []

total = 0
for order in orders:
    total += order['sum']
print('Заказов:', len(orders))
print('Сумма:', total)
Вывод
Заказов: 0
Сумма: 0

Ничего не сломалось — так и задумано: тело цикла не выполнилось ни разу, потому что перебирать было нечего. Пустой список — законное значение, и отчёт честно сказал «Заказов: 0». Беда начинается там, где источник данных пуст по ошибке: файл прочитали не так, запрос вернул не то поле, переменная перезаписалась раньше. Приём, который спасает: печатать длину того, что собрались перебирать, до цикла.

Сложение или присваивание: сбитый счётчик

Ошибка с накопителем выглядит не как падение, а как неправильное число — и поэтому её находят последней. Внутри цикла кто-то ставит total = 0 вместо total += x, и на каждой итерации накопленное стирается.

аккумулятор, который стирает сам себя
prices = [89, 42, 590]

total = 0
for p in prices:
    total = 0
    total += p
print(total)
Вывод
590

590 вместо 721. Число не выглядит подозрительно — оно похоже на настоящую сумму, просто неправильную. Обратите внимание, где стоит обнуление: если перенести total = 0 перед цикл, всё заработает. Отсюда правило: накопитель инициализируют перед циклом и внутри тела только вносят вклад. Строка total = 0 внутри тела — почти всегда ошибка.

Цикл считает не то: проверка инварианта

Самый дорогой класс ошибок — тот, где программа не падает и не зависает, а считает. Чтобы поймать такое, нужна опора, на которую можно опереться: утверждение, которое верно после каждой итерации.

сверка на каждом шаге
marks = [4, 5, 3]
count = 0

for m in marks:
    count += 1
    if count <= len(marks):
        print('обработано', count, 'из', len(marks))

print('всего:', count)
Вывод
обработано 1 из 3
обработано 2 из 3
обработано 3 из 3
всего: 3

Это и есть инвариант: после каждой итерации обработано ровно столько элементов, сколько шагов сделано. Печать внутри цикла показывает, что счётчик и длина списка идут в ногу. Когда числа в отчёте не сходятся, такая сверка обычно находится первой: расхождение видно на конкретном шаге, а не «где-то в конце».

ПолкаСимптомЛечение
ПадениеIndentationError, NameErrorотступ тела, проверка имён переменных
Молчаливая логикапропуски и лишние шаги в выводеenumerate вместо ручных range, новый список вместо правки старого
Зависаниепечать не кончаетсяизменяемая переменная условия, лимит итераций
Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

for i in range(2):
    print(i)
print('после:', i)
items = [10, 20, 30, 40]
for x in items:
    if x == 20:
        items.remove(x)
print(items)
total = 0
for p in [50, 25]:
    total = 1
    total += p
print(total)
Проверь себя
0 / 8

1. Что произойдёт при запуске цикла for, тело которого написано без отступа?

2. Что такое off-by-one в циклах?

3. Почему удалять элементы из списка прямо во время for опасно?

4. Как правильно отфильтровать список во время перебора?

5. В теле while написано battery == battery - 20. Что произойдёт?

6. Что произойдёт, если удалять ключи из словаря прямо во время перебора по нему?

7. Почему цикл по пустому списку — это проблема, а не мелочь?

8. Цикл печатает 590 вместо правильной суммы 721. Где искать причину?

Карточки терминов
Запомнено: 0 / 9
Практика

Цикл в стартер-коде пропускает первый элемент списка items = ['а', 'б', 'в'] из-за off-by-one. Перепишите его через enumerate так, чтобы печаталась каждая строка вида «0 а» — индекс и элемент.

practice.py
Вопросы и ответы по уроку

Почему цикл for не печатает первый или последний элемент?

Это off-by-one — ошибка границ range. Правая граница range не включается, а range(1, ...) ещё и пропускает индекс 0. Самое надёжное решение — enumerate: он сам раздаёт правильные номера без ручных границ.

Урок 4: enumerate

Можно ли удалять элементы из списка в цикле for?

Нельзя: перебор идёт по номерам позиций, и после удаления соседний элемент сдвигается на место удалённого — курсор его перескакивает. Соберите нужные элементы в новый список через append или генератор списка, а старый не трогайте.

Почему цикл while зависает, хотя условие написано правильно?

Скорее всего, переменная условия не меняется в теле: забыли шаг, или вместо присваивания записано сравнение (двойное равно). Проверьте: в каждой итерации должно быть действие, приближающее условие к ложности.

Урок 5: цикл while

Как быстро найти логическую ошибку в цикле?

Три приёма: печатный контроль — print ключевых переменных внутри каждой итерации; уменьшенные данные — список из трёх элементов проверяется глазами; проверка краёв — первый и последний шаги, где чаще всего живёт off-by-one.

Что такое IndexError и как его избежать в цикле?

Выход за границу списка: цикл обращается к индексу, которого нет. Обычно виновата арифметика в границах range. Лечится через len(list) — range(len(marks)) даёт ровно все существующие индексы, а enumerate делает это без ручного счёта.

Урок 4: enumerate

Почему удаление элемента из списка во время for не вызывает ошибки, а из словаря — вызывает?

Список после удаления сжимается, и курсор перебора просто перескакивает соседа — Python считает это допустимым. Словарь при удалении меняет размер, а перебор идёт по внутренней таблице: интерпретатор бросает RuntimeError. В обоих случаях лечение одно — собирать результат в новую структуру, не трогая перебираемую.

Цикл отработал, но обработал ноль элементов. Как это понять?

Напечатайте len(data) перед циклом: если длина нулевая, источник данных пуст — ищите ошибку не в теле цикла, а там, где данные взялись. Пустой список сам по себе не ошибка, но в отчёте он выглядит как «всё посчитано».

Понравился урок? Сошлитесь на него

«Ошибка в цикле стоит дороже обычной: она повторяется.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.