Вложенные циклы в Python: приёмы, скорость и когда их избегать
Этот урок переведён с английского оригинала.Read in EnglishLeer en españolСообщить об ошибке в переводе
Представьте электронную таблицу. В ней есть строки и столбцы. Чтобы прочитать каждую ячейку, вы проходите по каждой строке, а в каждой строке по каждому столбцу. Получается два цикла, один внутри другого. Это и есть вложенный цикл.
Вложенные циклы встречаются в программировании повсюду: при выводе узоров из символов, поиске по таблицам, переборе комбинаций и обработке изображений пиксель за пикселем. Это мощный инструмент, но без должной осторожности такие циклы могут работать неожиданно медленно.
В этом уроке вы узнаете, как работают вложенные циклы, разберёте практические примеры с таблицами и комбинациями, поймёте, почему такие циклы бывают медленными, и узнаете, когда лучше выбрать другой подход.
Как работают вложенные циклы в Python?
Вложенный цикл представляет собой цикл внутри другого цикла. Сначала запускается внешний цикл. На каждой итерации внешнего цикла внутренний цикл выполняется полностью, от начала до конца.
(1,1) (1,2) (1,3) (2,1) (2,2) (2,3) (3,1) (3,2) (3,3)
Главная мысль: если внешний цикл выполняется n раз, а внутренний m раз, то код внутри внутреннего цикла выполнится всего n x m раз. Три строки и три столбца дают 9 вызовов print.
Total iterations: 15
Работа с двумерными данными (сетки и таблицы)
Чаще всего вложенные циклы нужны для работы с двумерными данными. Список списков в Python представляет сетку, таблицу или матрицу. Внешний цикл проходит по строкам, а внутренний по столбцам каждой строки.
1 2 3 4 5 6 7 8 9
Иногда нужны не только значения, но и индексы строки и столбца. Для этого используйте enumerate().
X found at row 0, col 0 X found at row 0, col 2 X found at row 1, col 1 X found at row 2, col 2
Вот практический пример: составляем таблицу умножения.
1 2 3 4 5 2 4 6 8 10 3 6 9 12 15 4 8 12 16 20 5 10 15 20 25
Перебор комбинаций с помощью вложенных циклов
Вложенные циклы отлично подходят для перебора всех комбинаций элементов из двух (или более) групп. Внешний цикл выбирает элемент из первой группы, а внутренний из второй.
All t-shirt options: red - S red - M red - L blue - S blue - M blue - L
Из 2 цветов и 3 размеров получается 6 комбинаций: каждый цвет соединяется с каждым размером. Это называется декартовым произведением двух списков.
Pairs that sum to 10: 1 + 9 = 10 3 + 7 = 10
Рисуем узоры с помощью вложенных циклов
Вывод узоров из символов считается классическим упражнением для понимания вложенных циклов. Каждой строкой узора управляет внешний цикл, а количеством символов в строке управляет внутренний.
* ** *** **** *****
Хитрость в том, что диапазон внутреннего цикла зависит от переменной внешнего цикла. В строке 1 внутренний цикл выполняется один раз, в строке 2 дважды, в строке 5 пять раз. Так и получается лесенка.
***** **** *** ** *
Производительность: почему вложенные циклы бывают медленными
Один цикл по 1 000 элементов выполняется 1 000 раз. Вложенный цикл по 1 000 элементов выполняется 1 000 x 1 000 = 1 000 000 раз. Это миллион операций. Добавьте третий уровень вложенности, и вы дойдёте до миллиарда. Вложенные циклы перемножаются, и числа растут очень быстро.
Специалисты по информатике описывают это с помощью нотации «O большое» (Big O). Один цикл имеет сложность O(n): время растёт линейно вместе с объёмом данных. Вложенный цикл имеет сложность O(n²): время растёт пропорционально квадрату объёма данных. Удвойте входные данные, и работы станет вчетверо больше.
Когда стоит избегать вложенных циклов
Не каждой задаче, которая с виду требует вложенных циклов, они действительно нужны. Часто есть более быстрый способ с множествами, словарями или встроенными функциями.
nums = [3, 1, 4, 1, 5, 9]
has_dup = False
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] == nums[j]:
has_dup = True
print(has_dup) # Truenums = [3, 1, 4, 1, 5, 9]
has_dup = len(nums) != len(set(nums))
print(has_dup) # TrueВерсия с множеством не только короче, она несравнимо быстрее. Для 10 000 элементов версия с вложенным циклом выполняет 50 миллионов сравнений, а версия с множеством около 10 000 операций. Это в 5 000 раз быстрее.
students = [
{'name': 'Alice', 'id': 101},
{'name': 'Bob', 'id': 102},
{'name': 'Charlie', 'id': 103}
]
ids_to_find = [103, 101]
for target_id in ids_to_find:
for s in students:
if s['id'] == target_id:
print(s['name'])students = [
{'name': 'Alice', 'id': 101},
{'name': 'Bob', 'id': 102},
{'name': 'Charlie', 'id': 103}
]
ids_to_find = [103, 101]
lookup = {s['id']: s['name'] for s in students}
for target_id in ids_to_find:
print(lookup[target_id])Практические упражнения
Эти упражнения закрепят ваше понимание вложенных циклов. Они начинаются с простых узоров и постепенно переходят к задачам из реальной жизни.
Выведите прямоугольник из звёздочек: 3 строки и 5 столбцов. В каждой строке должно быть 5 звёздочек без пробелов между ними.
Ожидаемый вывод:
*****
*****
*****Не запуская код, предскажите, что он выведет. Посчитайте, сколько всего строк будет в выводе. Затем запустите код и проверьте себя.
Выведите числовой треугольник, в котором каждая строка состоит из повторённого номера этой строки. В треугольнике 4 строки.
Ожидаемый вывод:
1
22
333
4444Дана сетка чисел. С помощью вложенных циклов вычислите и выведите сумму всех значений в сетке.
Ожидаемый вывод:
45Даны списки верха (tops) и низа (bottoms) одежды. Выведите все возможные комплекты, а после них общее количество комплектов.
Ожидаемый вывод:
shirt + jeans
shirt + shorts
sweater + jeans
sweater + shorts
hoodie + jeans
hoodie + shorts
Total outfits: 6Этот код должен выводить сетку координат 3x3, но в нём есть ошибка. Каждая строка сетки должна выводиться на отдельной строке, x меняется от 0 до 2, и y тоже от 0 до 2.
Ожидаемый вывод:
(0,0) (0,1) (0,2)
(1,0) (1,1) (1,2)
(2,0) (2,1) (2,2)Найдите и исправьте ошибку.
Итоги: вложенные циклы в Python
Вот что вы узнали о вложенных циклах:
| Понятие | Главная мысль |
|---|---|
| Вложенный цикл | Цикл внутри другого цикла |
| Общее число итераций | Итерации внешнего цикла x итерации внутреннего |
| Двумерные данные | Внешний цикл = строки, внутренний цикл = столбцы |
| Комбинации | Каждый элемент группы A в паре с каждым элементом группы B |
| Производительность | O(n²): вдвое больше входных данных, вчетверо больше работы |
| Альтернативы | Множества и словари часто заменяют вложенные циклы |
Практические правила:
'*' * n) часто заменяет внутренний цикл в узорахЧто дальше?
В следующем уроке, [Как читать сообщения об ошибках Python](/python/python-error-messages), вы научитесь расшифровывать сообщения об ошибках, исправлять самые частые баги и перестанете бояться трассировок (traceback). Умение отлаживать код облегчает всё остальное.