$ sudo teach IT
Модуль 4 · Циклы · Урок 4.4

Вложенные циклы

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

Теория + практика~30 минутНовичок

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

Что такое вложенный цикл

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

Официально такая конструкция называется вложенный цикл (nested loop): один цикл (внешний) содержит в своём теле ещё один цикл (внутренний). В Python это устроено буквально так, как звучит — тело внутреннего цикла пишется с дополнительным отступом внутри тела внешнего.

for i in range(3):
    for j in range(2):
        print(i, j)

Разберём по кусочкам:

for i in range(3): — внешний цикл, переменная i пробежит значения 0, 1, 2. Это три его шага.

for j in range(2): — эта строка написана с отступом внутри тела внешнего цикла, значит это и есть внутренний, вложенный цикл. На каждом шаге i он запускается заново и полностью проходит свои значения j: 0, 1.

print(i, j) — ещё один отступ, значит это тело именно внутреннего цикла. Выполнится на каждом его шаге.

Пройдём весь путь программы вручную: i = 0 — внутренний цикл печатает 0 0, потом 0 1; i = 1 — внутренний цикл начинается заново и печатает 1 0, потом 1 1; i = 2 — снова с нуля: 2 0, потом 2 1. Итого 6 строк вывода — по числу шагов внешнего (3), умноженному на число шагов внутреннего (2).

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

Таблица умножения — классический пример

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

Официально здесь ничего нового: та же пара for, вложенная друг в друга, плюс уже знакомый параметр end у print(), который позволяет заменить перенос строки на другой символ.

for i in range(1, 10):
    for j in range(1, 10):
        print(i * j, end="\t")
    print()

Разберём каждую строку:

for i in range(1, 10) — i пробегает значения 1, 2, 3, ..., 9. Это номер строки таблицы.

for j in range(1, 10) — j тоже пробегает 1, 2, ..., 9. Это номер столбца, и на каждой новой строке он стартует заново.

print(i * j, end="\t") — печатаем произведение i на j. end="\t" означает: вместо переноса строки после числа поставить символ табуляции, чтобы числа не слипались, а выстроились в столбики.

print() — вызов без аргументов ставит обычный перенос строки. Он написан с отступом как for j, значит относится к телу внешнего цикла: сработает один раз после того, как внутренний цикл допечатает всю строку.

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

Сколько раз всё это выполнится

Простыми словами: если в одном кабинете 9 парт и кабинетов тоже 9, инспектор проверит 9 × 9 = 81 парту, а не 9 + 9 = 18. Каждый кабинет добавляет не парты, а ещё один полный обход всех парт. С циклами то же самое: они не складываются, а перемножаются.

Официально это называют просто числом итераций тела внутреннего цикла. Правило простое:

число шагов внешнего цикла × число шагов внутреннего = сколько раз выполнится самая внутренняя строка кода.

Примеры:

range(3) и range(5) — 3 × 5 = 15 раз

range(10) и range(10) — 10 × 10 = 100 раз

range(100) и range(100) — 100 × 100 = 10 000 раз

Данных стало в 10 раз больше — а работы в 100 раз. Такой рост даже имеет короткое обозначение — O(n × m), где n и m — число шагов внешнего и внутреннего циклов. Формулу запоминать необязательно, но полезна сама привычка прикидывать «сколько шагов сделает этот код» — на больших объёмах вложенные циклы могут заметно тормозить программу.

break и continue во вложенных циклах

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

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

for i in range(1, 4):
    for j in range(1, 10):
        if i * j > 15:
            break
        print(i, j)
    print("строка", i, "закончена")

if i * j > 15: — проверяем, не превысило ли произведение 15. Уже знакомое условие.

break — написан внутри for j, значит остановит именно его, текущий проход по j.

При i = 2 цикл остановится на j = 8 (2 × 8 = 16), но строка "строка 2 закончена" всё равно напечатается: break не тронул внешний цикл, тот спокойно перейдёт к i = 3.

continue ведёт себя так же локально — пропускает только текущий шаг того цикла, в котором стоит:

for i in range(1, 4):
    for j in range(1, 4):
        if j == 2:
            continue
        print(i, j)

На каждой строке (каждом значении i) пропадёт ровно одна пара — та, где j == 2. Строки при этом не потеряются: continue относится только к внутреннему циклу, внешний про него ничего не знает.

Если нужно выйти сразу из двух уровней — отдельной команды для этого в Python нет. Обычно заводят переменную-флаг: как только находится нужное значение, флаг ставится в True, внутренний цикл делает свой break, а сразу после него внешний проверяет флаг и тоже выходит.
found = False

for i in range(5):
    for j in range(5):
        if i * j == 6:
            found = True
            break
    if found:
        break

found = True и break срабатывают внутри вложенного цикла, когда пара найдена. Строка if found: break стоит в теле внешнего цикла (один отступ, а не два) — она проверяется после каждого завершения внутреннего и при поднятом флаге останавливает уже сам внешний цикл.

Когда нужно не напечатать, а собрать строку

Простыми словами: до сих пор мы сразу печатали каждый результат. Но иногда нужно не напечатать, а собрать результат целиком — например, чтобы проверить его один раз в конце. Тогда вместо print() внутри цикла добавляем кусочек к строке-результату, а печатаем её только когда оба цикла закончили работу.

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

n = 4
i = 1
result = ""

while i <= n:
    j = 1
    line = ""
    while j <= i:
        line += "*"
        j += 1
    result += line
    if i < n:
        result += "\n"
    i += 1

print(result)

i = 1, result = "" — i будет номером текущей строки треугольника, result — пустая строка, куда мы сложим всё готовое.

while i <= n: — внешний цикл, один шаг — одна строка треугольника, всего их будет n.

j = 1, line = "" — в начале каждой строки заводим свой счётчик j и свою пустую строку line для звёздочек именно этой строки.

while j <= i: — внутренний цикл: он повторится ровно i раз, то есть на второй строке треугольника — дважды, на третьей — трижды.

line += "*" и j += 1 — на каждом шаге внутреннего цикла добавляем одну звёздочку к line и увеличиваем счётчик, иначе цикл никогда не остановится.

result += line — после того как внутренний цикл собрал звёздочки текущей строки, добавляем их к общему результату.

if i < n: result += "\n" — перенос строки нужен только между строками треугольника, а не после самой последней. Проверка «это ещё не последняя строка» как раз это и гарантирует.

i += 1 — переходим к следующей строке внешнего цикла. Без этой строки цикл тоже зациклится навсегда.

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

n = 2
m = 2
i = 1
result = ""

while i <= n:
    j = 1
    while j <= m:
        if result:
            result += "\n"
        result += f"{i} {j}"
        j += 1
    i += 1

print(result)

if result: — пустая строка в условии считается ложью, непустая — истиной. Пока result пуст (перед самой первой парой), перенос не добавляется. Дальше result уже не пуст, и перед каждой новой парой появляется "\n".

result += f"{i} {j}" — добавляем саму пару в формате «число пробел число» через уже знакомую f-строку.

Для n = 2 и m = 2 получится: 1 1, 1 2, 2 1, 2 2 — каждая пара на своей строке, без лишнего переноса в начале и в конце.

Частые ошибки

Забыли сбросить счётчик внутреннего цикла

i = 1
j = 1
while i <= 3:
    while j <= 3:
        print(i, j)
        j += 1
    i += 1
Внутренний цикл сработает только один раз — при i = 1 счётчик j дойдёт до 4, и условие j <= 3 перестанет выполняться. Для i = 2 и i = 3 оно уже не выполнится вообще: j так и остался равен 4. Строку j = 1 нужно писать внутри тела внешнего цикла, чтобы счётчик каждый раз начинал заново.

Отступ сдвинул строку в чужое тело

for i in range(1, 10):
    for j in range(1, 10):
        print(i * j, end="\t")
        print()
Таблица развалится в столбик — здесь print() сдвинут на тот же отступ, что и print(i * j, ...), значит относится к телу внутреннего цикла и сработает после каждого числа, а не после каждой строки. Нужен отступ на один уровень меньше.

Лишний перенос строки в самом конце

result = ""
i = 1
while i <= 3:
    result += "*" * i
    result += "\n"
    i += 1
После последней строки останется лишний символ переноса — "\n" добавляется на каждом шаге без исключения, в том числе после самой последней строки. Если проверка ждёт результат без завершающего переноса, такой код её не пройдёт. Нужна проверка вроде if i < n:, которая добавляет перенос только между строками.

Думают, что break выходит из обоих циклов сразу

Частое ожидание новичков — что один break внутри вложенного цикла остановит сразу и внешний цикл тоже. На деле он останавливает только тот цикл, в чьём теле написан. Если нужно выйти из обоих сразу — понадобится переменная-флаг, как в примере с break и continue выше.

Что важно запомнить

Вложенный цикл — это цикл в теле другого цикла. Внутренний выполняется целиком на каждом шаге внешнего и каждый раз начинается заново.
Общее число повторений самой внутренней строки — шаги внешнего × шаги внутреннего, а не сумма.
break и continue действуют только на ближайший цикл. Чтобы выйти из двух уровней сразу, нужен флаг-переменная.
Отступы во вложенных циклах определяют, какому циклу принадлежит строка — ошибка в отступе меняет поведение всей программы.
Если нужно не напечатать, а собрать результат — заводите пустую строку до циклов и добавляйте к ней через +=, а разделитель ставьте по условию, чтобы не осталось лишнего в начале или в конце.

Проверьте себя

5 вопросов

Звёздный треугольник

Дано число n — высота треугольника. Постройте строку result, содержащую треугольник из звёздочек (*): первая строка — 1 звезда, вторая — 2, и так до n строк. Строки разделяйте переносом строки \n, без переноса в конце последней строки.

Пример для n = 3 (значение result, если напечатать его через print):

*
**
***

Используйте вложенные циклы while.

Все пары чисел

Даны два числа n и m. Постройте строку result со всеми возможными парами (i, j), где i пробегает значения от 1 до n, а j — от 1 до m.

Каждую пару добавляйте отдельной строкой в формате i j (число, пробел, число), строки разделяйте переносом строки \n.

Пример для n = 2, m = 3 (значение result, если напечатать его через print):

1 1
1 2
1 3
2 1
2 2
2 3

Важно: после последней пары переноса строки быть не должно — строка не должна заканчиваться символом \n. Удобный приём: добавлять перенос перед новой парой, а не после предыдущей, и не добавлять его вовсе, если result ещё пустой.

Используйте вложенные циклы while.

Таблица умножения 9 на 9

Постройте строку result с полной таблицей умножения от 1 до 9. В каждой строке таблицы — 9 чисел, разделённых одним пробелом, строки разделяйте переносом строки \n (без переноса в конце последней строки).

Пример первых двух строк (значение result, если напечатать его через print):

1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
...

Используйте вложенные циклы for с range.

Шахматная доска

Дано число n — размер доски (n на n клеток). Постройте строку result с шахматным узором: клетки чередуются — # и ., как на настоящей доске. Левая верхняя клетка — всегда #. Строки разделяйте переносом строки \n, без переноса в конце последней строки.

Пример для n = 4 (значение result, если напечатать его через print):

#.#.
.#.#
#.#.
.#.#

Подсказка: чтобы узнать, какой символ поставить в клетку строки i и столбца j, посмотрите на остаток от деления суммы i + j на 2 — уже знакомый оператор %.

Используйте вложенные циклы for с range и знакомое условие if / else.