Skip to content

Глава 8. Циклы `for` и `while`: автоматизация рутины

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


8.1 Цикл for: обход списков и последовательностей

Зачем это нужно. В выгрузке не один балл, а двести. Копировать print() двести раз невозможно — цикл for выполняет один и тот же блок кода для каждого элемента списка: пройти по всем баллам класса, напечатать уведомление каждому должнику.

// PYTHON_EXECUTOR (Pyodide Wasm)
Балл: 87 — зачёт
Балл: 45 — незачёт
Балл: 92 — зачёт
Балл: 63 — зачёт
Балл: 78 — зачёт

Переменная score — не отдельная сущность, а временное хранилище текущего элемента. На первом проходе в ней лежит 87, на втором 45, и так до конца списка. Имя выбирается по смыслу данных: for score in group_scores, for student in students, for row in csv_rows — так цикл читается как фраза на русском.

Обходить можно любую последовательность, не только список:

// PYTHON_EXECUTOR (Pyodide Wasm)

⚠️ Двоеточие в конце строки for ...: и отступ в 4 пробела обязательны. Без отступа Python не понимает, где начинается тело цикла, и падает с IndentationError: expected an indented block.

⚠️ Подводный камень

Нельзя удалять элементы из списка, по которому идёт итерация. Python обходит список по внутреннему счётчику индексов, а .remove() сдвигает элементы влево — счётчик перескакивает через соседа, и тот остаётся необработанным. Traceback не появится: ошибка тихая, часть мусора просто доедет до отчёта.

// PYTHON_EXECUTOR (Pyodide Wasm)
[87, -1, 45, 92]
ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«Индекс сместился, беру следующий по счёту»Удалили первый -1 — список сжался, и второй -1 встал на позицию, которую цикл уже прошёл. Итерируйтесь по списку только для чтения, а результат складывайте в новый список.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: исходная выгрузка остаётся неизменной — если фильтр окажется неверным, всегда видно, что именно пришло из системы. Второй допустимый вариант — итерироваться по копии for score in module_scores[:], а удалять из оригинала. Но «собрать новый список» надёжнее и читается лучше: сразу понятно, что попадёт в расчёт.


8.2 Накопители: паттерны «Счётчик» и «Сумматор»

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

// PYTHON_EXECUTOR (Pyodide Wasm)
Сумма баллов: 365
Сдали: 4 из 5
Средний балл: 73.0 | Лучший: 92

Три накопителя — три роли:

ПаттернИнициализацияШаг в цикле
Сумматорtotal_score = 0total_score += score
Счётчикpassed_count = 0passed_count += 1
Максимумbest_score = 0if score > best_score: best_score = score

⚠️ С минимумом нулевая инициализация ломается: worst_score = 0 никогда не перезапишется, потому что балл не бывает меньше нуля. Стартовать нужно от заведомо большого значения — worst_score = 100 — или от первого элемента списка worst_score = group_scores[0].

⚠️ Подводный камень

Ловушка первая — накопитель объявлен внутри цикла. На каждом проходе он сбрасывается в ноль, поэтому в конце в нём остаётся вклад только последнего элемента.

// PYTHON_EXECUTOR (Pyodide Wasm)

Ловушка вторая — накопитель не объявлен вовсе. Здесь Python молчать не станет:

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 4, in <module>
total_score += score
NameError: name 'total_score' is not defined
ОшибкаЧто значит для PythonЧеловеческий перевод
NameError«Имя total_score в этой области не определено»+= читает старое значение переменной перед тем, как записать новое. Читать нечего — накопитель не создан. Поставьте total_score = 0 до for.
— (тихая ошибка)«Присваиваю ноль на каждой итерации»Строка total_score = 0 попала внутрь тела цикла. Итог обнуляется перед каждым прибавлением, в отчёт уходит значение последней строки выгрузки.

Мнемоника: инициализация — до цикла, изменение — внутри цикла. Если строка начинается с имя = 0, ей место над for.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: накопители объявлены до цикла, поэтому после пустой выгрузки они честно равны нулю, а не рушат программу. А вот total_score / len(group_scores) на пустом списке даст ZeroDivisionError — среднее по нулю строк математически не существует. Проверка len() > 0 перед делением обязательна везде, где данные приходят извне.


8.3 Функция range() и итерирование по индексам

Зачем это нужно. Иногда обходить нечего — нужно выполнить действие ровно N раз: сгенерировать 5 пустых колонок в шаблоне отчёта, распечатать шкалу баллов с шагом 10. А иногда нужен не сам элемент, а его номер: чтобы пронумеровать строки ведомости или сопоставить два параллельных списка.

// PYTHON_EXECUTOR (Pyodide Wasm)

Связка range(len(...)) даёт доступ по индексу — это единственный способ идти по двум параллельным спискам одновременно:

// PYTHON_EXECUTOR (Pyodide Wasm)
1. Иванов — 87 б.
2. Петрова — 45 б.
3. Сидоров — 92 б.

⚠️ Подводный камень

Правая граница в range() не включается. То же правило, что и у срезов из главы Строки и f-строки: range(1, 5) выдаёт 1, 2, 3, 4 — четыре значения, а не пять. Ошибка «на единицу» (off-by-one) — самая частая в отчётах: последний ученик или последняя попытка теряются.

// PYTHON_EXECUTOR (Pyodide Wasm)
Попытка 1 из 5
Попытка 2 из 5
Попытка 3 из 5
Попытка 4 из 5

Пятой попытки в отчёте нет. Ошибка тихая — скрипт отработал «успешно».

Вторая ловушка того же семейства — range(len()) по разным спискам. В выгрузке балл выгрузился не у всех, списки разъехались по длине, и здесь Python уже падает:

// PYTHON_EXECUTOR (Pyodide Wasm)
Иванов — 87
Петрова — 45
Traceback (most recent call last):
File "report.py", line 5, in <module>
print(f'{students[i]} — {scores[i]}')
IndexError: list index out of range
ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«stop — граница, за которую я не захожу»range(1, 5) — это 1, 2, 3, 4. Для нумерации от единицы до N пишите range(1, count + 1).
IndexError«Индекс 2 за пределами списка длиной 2»Длину взяли у одного списка, а индексируете другой — короче. Считайте длину по короткому списку либо проверяйте len(students) == len(scores) до цикла.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)

Пояснение логики: + 1 в правой границе — стандартная поправка на невключение, её проще запомнить как обязательный элемент шаблона «пронумеровать N штук». Проверка равенства длин ставится до цикла: расхождение в выгрузке — это событие для отчёта, а не повод уронить скрипт на середине.

Бонус: когда нужны и номер, и элемент одновременно, есть более короткая запись — enumerate():

// PYTHON_EXECUTOR (Pyodide Wasm)

range(len(...)) остаётся незаменимым там, где по индексу нужно достать элемент другого списка.


8.4 Прерывание и пропуск: break и continue

Зачем это нужно. continue — «этот элемент мне не подходит, дальше»: пропустить пустые строки, не явившихся учеников, пропущенные уроки. break — экстренный останов: нашли в выгрузке признак фальсификации, дальше считать бессмысленно, итог всё равно недостоверен.

// PYTHON_EXECUTOR (Pyodide Wasm)
Учтён балл: 78
Урок пропущен, не учитываем
Учтён балл: 92
⚠️ Балл 105 выше максимума — расчёт остановлен
Итого набрано: 170

Разница в одном: continue бросает текущий проход и берёт следующий элемент, break бросает весь цикл целиком. Балл 64 после break не обработан — и это ровно то поведение, которое требовалось.

⚠️ Подводный камень

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

// PYTHON_EXECUTOR (Pyodide Wasm)
Группа [78, 92] обработана
Найден битый балл
Группа [64, 105] обработана
Группа [88, 90] обработана

Группа с баллом 105 помечена как обработанная, и скрипт продолжил считать остальные — хотя решение было «останов при любой фальсификации».

ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«break прерывает цикл, в котором он написан»Ждали выхода из обоих циклов, получили выход из внутреннего. Заведите флаг (has_error = True), а на внешнем уровне проверьте его и сделайте второй break.

Вторая ловушка — не техническая, а читательская: если break и continue рассыпаны по всему телу цикла вперемешку с расчётами, логику «что и когда учитывается» невозможно восстановить глазами. Это та самая «спагетти-логика».

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)
Группа [78, 92] обработана
⚠️ Расчёт прерван: обнаружены недостоверные данные

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

И правило чтения: все continue-фильтры ставятся в самом начале тела цикла, до расчётов. Тогда цикл читается в два блока — «сначала выбрасываем мусор, потом считаем по чистым данным»:

// PYTHON_EXECUTOR (Pyodide Wasm)

8.5 Цикл while: обработка условий и интерактивный ввод

Зачем это нужно. for знает заранее, сколько будет проходов — столько, сколько элементов. while не знает: он повторяет блок, пока условие истинно. Классика для аналитика — запрашивать балл, пока оператор не введёт корректное значение от 0 до 100.

// PYTHON_EXECUTOR (Pyodide Wasm)
Балл от 0 до 100: 105
⚠️ Балл вне диапазона, повторите ввод
Балл от 0 до 100: 87
Принят балл: 87

Второй рабочий шаблон — счётчик попыток, чтобы оператор не застрял навсегда:

// PYTHON_EXECUTOR (Pyodide Wasm)

⚠️ Подводный камень

Бесконечный цикл (infinite loop). Если переменная из условия внутри тела не меняется, условие остаётся истинным навсегда. Программа не падает — она «зависает» и печатает одно и то же, пока её не убьют вручную через Ctrl + C.

// PYTHON_EXECUTOR (Pyodide Wasm)
Проверяем следующую строку выгрузки...
Проверяем следующую строку выгрузки...
Проверяем следующую строку выгрузки...
^C
Traceback (most recent call last):
File "report.py", line 4, in <module>
print('Проверяем следующую строку выгрузки...')
KeyboardInterrupt

Ловушка вторая, коварнее: continue в while перепрыгивает через шаг счётчика, если тот стоит в конце тела.

// PYTHON_EXECUTOR (Pyodide Wasm)

Скрипт напечатает Иванов;87 и навсегда зависнет на пустой строке с индексом 1.

ОшибкаЧто значит для PythonЧеловеческий перевод
— (тихая ошибка)«Условие всё ещё True, повторяю тело»Забыли изменить переменную условия. В теле while обязательно должна быть строка, которая приближает выход: attempts_left -= 1, row_index += 1, is_valid = True или break.
KeyboardInterrupt«Выполнение прервано пользователем (Ctrl + CЭто не ошибка кода, а способ убить зависший цикл. Увидели её после своего while — ищите непеременяющееся условие.

Правильный паттерн

// PYTHON_EXECUTOR (Pyodide Wasm)
Иванов — 87 б.
Петрова — 45 б.

Пояснение логики: счётчик увеличивается до всех continue и break, поэтому ни один путь исполнения не может «застрять» на текущей строке. Текущий элемент заранее скопирован в current_row — читается лучше, чем csv_rows[row_index] в пяти местах.

И главный вывод раздела: если количество проходов известно заранее — берите for. Пример выше написан на while только для демонстрации; в реальном коде обход списка строк — это for current_row in csv_rows, где зациклиться невозможно по устройству. while оставляют для интерактивного ввода, ожидания события и повторных попыток.


8.6 Мини-словарик ошибок восьмой главы

ОшибкаЧто значит для PythonЧеловеческий перевод
NameError«Имя total_score не определено»Использовали накопитель, не объявив его до цикла. += требует, чтобы переменная уже существовала.
UnboundLocalError«Локальная переменная использована до присваивания»Тот же случай, но внутри функции: накопитель объявлен снаружи, а меняется внутри. Разбор — в главе Функции.
IndexError«Индекс за пределами списка»range(len(students)), а индексируется более короткий scores. Проверяйте длины до цикла.
ZeroDivisionError«Деление на ноль»Считали среднее по пустой выгрузке: total_score / len(group_scores), где длина 0.
TypeError«Объект int не является итерируемым»Написали for score in 87 — по одному числу пройти нельзя, нужна последовательность.
ValueError«Не могу превратить 'не явился' в число»int(input()) внутри while: оператор ввёл текст вместо балла.
IndentationError«Ожидался блок с отступом»Забыли отступ в теле цикла после for ...:.
KeyboardInterrupt«Прервано пользователем»Убили Ctrl + C зависший while — условие не менялось.
— (тихая ошибка)«Присваиваю ноль на каждой итерации»total_score = 0 внутри тела цикла: в итоге остался вклад только последнего элемента.
— (тихая ошибка)«stop не включается»range(1, 5) вместо range(1, 6) — последний ученик или последняя попытка потерялись.
— (тихая ошибка)«Индекс сместился после удаления»Удаляли элементы из списка во время итерации по нему — часть мусора осталась.
— (тихая ошибка)«continue — прыжок к проверке условия»В while счётчик стоял после continue и не выполнялся. Бесконечный цикл.

Самый обидный случай целиком — TypeError: 'int' object is not iterable. Он приходит, когда в переменной ждали список баллов, а пришёл один балл: так бывает, если выгрузка содержит единственную строку и система вернула не список, а скалярное значение.

// PYTHON_EXECUTOR (Pyodide Wasm)
Traceback (most recent call last):
File "report.py", line 3, in <module>
for score in group_scores:
TypeError: 'int' object is not iterable

«Итерируемый» — значит «состоящий из элементов, которые можно перебрать». Список, строка, кортеж — итерируемы; int и float — нет. Лечится нормализацией данных на входе:

// PYTHON_EXECUTOR (Pyodide Wasm)

Мини-кейс: расчёт конверсии прохождения курса

Задача: по списку баллов за модули посчитать сумму и средний балл ученика, пропустив пропущенные уроки (-1), посчитать конверсию прохождения (доля модулей, сданных на 60+) и прервать расчёт, если обнаружен факт фальсификации — балл выше 100.

// PYTHON_EXECUTOR (Pyodide Wasm)

Прогон на данных из кейса:

--- КОНВЕРСИЯ КУРСА: Иванов Иван ---
Учтено модулей: 5 | Пропущено: 2
Сумма баллов: 398 | Средний: 79.6 | Лучший: 95
Сдано на 60+: 4 из 5
Конверсия прохождения: 80%

Тот же скрипт на выгрузке с фальсификацией — module_scores = [78, -1, 92, 120, 88]:

--- КОНВЕРСИЯ КУРСА: Иванов Иван ---
⚠️ Расчёт прерван: найден балл выше максимума. Выгрузка требует проверки.

Что получилось: один проход по списку дал сразу шесть метрик — сумму, количество учтённых, количество пропусков, количество сдач, максимум и флаг достоверности. Все накопители объявлены до цикла, поэтому на пустой выгрузке скрипт не упадёт, а честно сообщит, что считать нечего. Фильтр пропусков стоит первым — continue отсекает -1 до всех расчётов, и средний балл считается по counted_modules, а не по len(module_scores): пропущенные уроки не занижают результат. Проверка на балл выше максимума ставит флаг и делает break, а решение «что печатать» принимается уже после цикла — по флагу, а не по факту прерывания.

⚠️ Одна дыра осталась: если в выгрузке вместо числа окажется строка 'не явился', сравнение score > max_score упадёт с TypeError: '>' not supported between instances of 'str' and 'int'. Защита от таких данных — try / except из главы Ошибки и исключения. А среднее и максимум по чистому списку можно считать и без ручных накопителей — готовыми функциями из главы Стандартная библиотека.


Чек-лист главы

  • Пишу for item in sequence: с двоеточием и отступом в 4 пробела
  • Даю переменной цикла говорящее имя: for score in group_scores, а не for x in data
  • Не удаляю элементы из списка, по которому иду, — собираю новый очищенный список
  • Объявляю накопители (total_score = 0, passed_count = 0) строго до цикла
  • Помню, что для минимума нулевая инициализация не работает — стартую от 100 или от первого элемента
  • Проверяю len(...) > 0 перед делением на количество, чтобы не поймать ZeroDivisionError
  • Помню, что правая граница range() не включается, и пишу range(1, count + 1) для нумерации от единицы
  • Перед range(len(...)) по двум спискам сверяю их длины
  • Ставлю все continue-фильтры в начале тела цикла, до расчётов
  • Знаю, что break выходит только из внутреннего цикла, и использую флаг для выхода из вложенных
  • В каждом while вижу строку, которая меняет условие, — иначе будет бесконечный цикл
  • В while увеличиваю счётчик до continue, а не после
  • Беру for, когда число проходов известно, и while — только для ввода и ожидания события
  • Узнаю NameError (накопитель не объявлен) и TypeError: 'int' object is not iterable (вместо списка пришло число)

Дальше: Функции