Один цикл умеет пройтись по одному ряду значений: числам от 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 относится только к внутреннему циклу, внешний про него ничего не знает.
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.