Перейти к содержимому
Editor

Вложенные циклы в Python: приёмы, скорость и когда их избегать

Средний25 min6 упр.80 XP

Этот урок переведён с английского оригинала.Read in EnglishLeer en españolСообщить об ошибке в переводе

0/6 упр.

Представьте электронную таблицу. В ней есть строки и столбцы. Чтобы прочитать каждую ячейку, вы проходите по каждой строке, а в каждой строке по каждому столбцу. Получается два цикла, один внутри другого. Это и есть вложенный цикл.

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

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

Как работают вложенные циклы в Python?

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

Простой вложенный цикл
for row in range(1, 4): # Outer loop: rows 1, 2, 3
for col in range(1, 4): # Inner loop: columns 1, 2, 3
print(f'({row},{col})', end=' ')
print() # New line after each row
Вывод
(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.

Считаем итерации вложенного цикла
# Counting total iterations
count = 0
for i in range(5): # 5 iterations
for j in range(3): # 3 iterations each time
count += 1
print(f'Total iterations: {count}') # 5 x 3 = 15
Вывод
Total iterations: 15

Работа с двумерными данными (сетки и таблицы)

Чаще всего вложенные циклы нужны для работы с двумерными данными. Список списков в Python представляет сетку, таблицу или матрицу. Внешний цикл проходит по строкам, а внутренний по столбцам каждой строки.

Перебираем двумерную сетку
# A 3x3 grid represented as a list of lists
grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Print every element
for row in grid:
for value in row:
print(value, end=' ')
print() # New line after each row
Вывод
1 2 3 
4 5 6 
7 8 9

Иногда нужны не только значения, но и индексы строки и столбца. Для этого используйте enumerate().

enumerate во вложенных циклах
grid = [
['X', 'O', 'X'],
['O', 'X', 'O'],
['O', 'O', 'X']
]
# Find all positions where X appears
for r, row in enumerate(grid):
for c, cell in enumerate(row):
if cell == 'X':
print(f'X found at row {r}, col {c}')
Вывод
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

Вот практический пример: составляем таблицу умножения.

Таблица умножения на вложенных циклах
# Multiplication table (1 through 5)
for i in range(1, 6):
for j in range(1, 6):
product = i * j
print(f'{product:4}', end='')
print()
Вывод
   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

Перебор комбинаций с помощью вложенных циклов

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

Все сочетания цветов и размеров
colors = ['red', 'blue']
sizes = ['S', 'M', 'L']
print('All t-shirt options:')
for color in colors:
for size in sizes:
print(f' {color} - {size}')
Вывод
All t-shirt options:
  red - S
  red - M
  red - L
  blue - S
  blue - M
  blue - L

Из 2 цветов и 3 размеров получается 6 комбинаций: каждый цвет соединяется с каждым размером. Это называется декартовым произведением двух списков.

Ищем пары с заданной суммой
# Finding pairs that add up to a target
numbers = [1, 3, 5, 7, 9]
target = 10
print(f'Pairs that sum to {target}:')
for i in range(len(numbers)):
for j in range(i + 1, len(numbers)): # Start j after i to avoid duplicates
if numbers[i] + numbers[j] == target:
print(f' {numbers[i]} + {numbers[j]} = {target}')
Вывод
Pairs that sum to 10:
  1 + 9 = 10
  3 + 7 = 10

Рисуем узоры с помощью вложенных циклов

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

Прямоугольный треугольник из звёздочек
# Right triangle pattern
n = 5
for row in range(1, n + 1):
for col in range(row):
print('*', end='')
print()
Вывод
*
**
***
****
*****

Хитрость в том, что диапазон внутреннего цикла зависит от переменной внешнего цикла. В строке 1 внутренний цикл выполняется один раз, в строке 2 дважды, в строке 5 пять раз. Так и получается лесенка.

Перевёрнутый треугольник (способ попроще)
# Inverted triangle
n = 5
for row in range(n, 0, -1):
print('*' * row)
Вывод
*****
****
***
**
*

Производительность: почему вложенные циклы бывают медленными

Один цикл по 1 000 элементов выполняется 1 000 раз. Вложенный цикл по 1 000 элементов выполняется 1 000 x 1 000 = 1 000 000 раз. Это миллион операций. Добавьте третий уровень вложенности, и вы дойдёте до миллиарда. Вложенные циклы перемножаются, и числа растут очень быстро.

Специалисты по информатике описывают это с помощью нотации «O большое» (Big O). Один цикл имеет сложность O(n): время растёт линейно вместе с объёмом данных. Вложенный цикл имеет сложность O(n²): время растёт пропорционально квадрату объёма данных. Удвойте входные данные, и работы станет вчетверо больше.

Сравниваем скорость одиночного и вложенного циклов
import time
# O(n) - single loop
n = 10000
start = time.time()
total = 0
for i in range(n):
total += i
single_time = time.time() - start
# O(n²) - nested loop
start = time.time()
total = 0
for i in range(n):
for j in range(n):
total += 1
nested_time = time.time() - start
print(f'Single loop: {single_time:.4f} seconds')
print(f'Nested loop: {nested_time:.4f} seconds')
print(f'Nested is {nested_time / max(single_time, 0.0001):.0f}x slower')

Когда стоит избегать вложенных циклов

Не каждой задаче, которая с виду требует вложенных циклов, они действительно нужны. Часто есть более быстрый способ с множествами, словарями или встроенными функциями.

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)  # True
O(n): используем множество
nums = [3, 1, 4, 1, 5, 9]
has_dup = len(nums) != len(set(nums))
print(has_dup)  # True

Версия с множеством не только короче, она несравнимо быстрее. Для 10 000 элементов версия с вложенным циклом выполняет 50 миллионов сравнений, а версия с множеством около 10 000 операций. Это в 5 000 раз быстрее.

O(n²): вложенный цикл для поиска значения
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'])
O(n): сначала строим словарь
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])

Практические упражнения

Эти упражнения закрепят ваше понимание вложенных циклов. Они начинаются с простых узоров и постепенно переходят к задачам из реальной жизни.

Упражнение 1: выведите прямоугольник
Напишите код

Выведите прямоугольник из звёздочек: 3 строки и 5 столбцов. В каждой строке должно быть 5 звёздочек без пробелов между ними.

Ожидаемый вывод:

*****
*****
*****
# Print a 3x5 rectangle of stars
Упражнение 2: предскажите вывод
Угадайте вывод

Не запуская код, предскажите, что он выведет. Посчитайте, сколько всего строк будет в выводе. Затем запустите код и проверьте себя.

for i in range(3):
for j in range(2):
print(f'{i}-{j}')
Упражнение 3: числовой треугольник
Напишите код

Выведите числовой треугольник, в котором каждая строка состоит из повторённого номера этой строки. В треугольнике 4 строки.

Ожидаемый вывод:

1
22
333
4444
# Print a number triangle with 4 rows
Упражнение 4: сумма всех значений сетки
Напишите код

Дана сетка чисел. С помощью вложенных циклов вычислите и выведите сумму всех значений в сетке.

Ожидаемый вывод:

45
grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Calculate the sum of all values
Упражнение 5: все комплекты одежды
Напишите код

Даны списки верха (tops) и низа (bottoms) одежды. Выведите все возможные комплекты, а после них общее количество комплектов.

Ожидаемый вывод:

shirt + jeans
shirt + shorts
sweater + jeans
sweater + shorts
hoodie + jeans
hoodie + shorts
Total outfits: 6
tops = ['shirt', 'sweater', 'hoodie']
bottoms = ['jeans', 'shorts']
# Print all combinations and the total count
Упражнение 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)

Найдите и исправьте ошибку.

for x in range(3):
for y in range(3):
print(f'({x},{y})', end=' ')
print()

Итоги: вложенные циклы в Python

Вот что вы узнали о вложенных циклах:

ПонятиеГлавная мысль
Вложенный циклЦикл внутри другого цикла
Общее число итерацийИтерации внешнего цикла x итерации внутреннего
Двумерные данныеВнешний цикл = строки, внутренний цикл = столбцы
КомбинацииКаждый элемент группы A в паре с каждым элементом группы B
ПроизводительностьO(n²): вдвое больше входных данных, вчетверо больше работы
АльтернативыМножества и словари часто заменяют вложенные циклы

Практические правила:

  • Думайте о размере данных, прежде чем вкладывать циклы друг в друга
  • Давайте переменным понятные имена (row/col, а не i/j)
  • Умножение строки ('*' * n) часто заменяет внутренний цикл в узорах
  • Для поиска элементов словарь или множество почти всегда быстрее вложенного цикла
  • Что дальше?

    В следующем уроке, [Как читать сообщения об ошибках Python](/python/python-error-messages), вы научитесь расшифровывать сообщения об ошибках, исправлять самые частые баги и перестанете бояться трассировок (traceback). Умение отлаживать код облегчает всё остальное.

    Похожие уроки

    Урок был полезен?