Циклы: for, while, range, enumerate, zip, break/continue

В Python два типа циклов: for для перебора коллекций и while для повторения по условию. В отличие от C, в Python нет цикла со счётчиком в стиле for(i=0; i<10; i++) - вместо него используется for i in range(10). Это синтаксически проще и идиоматичнее.

for - итерация по коллекции

fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

for перебирает любой iterable - объект, который умеет выдавать элементы один за другим. Это список, кортеж, строка, dict, set, генератор, файл (построчно). Как устроен протокол итерации изнутри - в уроке про итераторы.

for char in "Hello":
    print(char)  # H, e, l, l, o

for line in open("file.txt"):
    print(line)  # каждая строка файла

range - последовательность чисел

range создаёт ленивую последовательность целых:

for i in range(5):       # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 8):    # 2, 3, 4, 5, 6, 7
    print(i)

for i in range(0, 10, 2): # 0, 2, 4, 6, 8 - шаг 2
    print(i)

for i in range(10, 0, -1): # 10, 9, ..., 1 - обратно
    print(i)

Сигнатура: range(start=0, stop, step=1). stop не входит в результат - это полу-открытый интервал [start, stop).

`range(1000000)` не создаёт миллион чисел в памяти - это объект, который выдаёт числа по запросу. Чтобы получить список, нужно `list(range(1000000))`. Лень `range` экономит память на больших диапазонах.

enumerate - индекс + значение

Если в цикле нужен и индекс, и значение - не пиши руками i = 0; i += 1. Используй enumerate:

fruits = ["apple", "banana", "cherry"]

# Плохо
i = 0
for fruit in fruits:
    print(f"{i}: {fruit}")
    i += 1

# Хорошо
for i, fruit in enumerate(fruits):
    print(f"{i}: {fruit}")

# С начальным индексом
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}: {fruit}")  # 1, 2, 3

zip - параллельная итерация

zip итерирует несколько коллекций одновременно:

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]

for name, age in zip(names, ages):
    print(f"{name} - {age} лет")

Если длины разные, zip останавливается на самой короткой. С Python 3.10 есть strict=True для проверки одинаковой длины:

list(zip([1, 2], [3, 4, 5]))           # [(1, 3), (2, 4)] - тихо обрезалось
list(zip([1, 2], [3, 4, 5], strict=True)) # ValueError

Альтернатива - itertools.zip_longest для дополнения коротких:

from itertools import zip_longest

for a, b in zip_longest([1, 2], [3, 4, 5], fillvalue=0):
    print(a, b)
# (1, 3), (2, 4), (0, 5)

while - цикл по условию

n = 10
while n > 0:
    print(n)
    n -= 1

while используется когда количество итераций неизвестно заранее: чтение до конца, ожидание события, polling. Для итерации по коллекции - for.

Бесконечный цикл с выходом по условию:

while True:
    line = input("> ")
    if line == "quit":
        break
    process(line)

break и continue

break выходит из цикла полностью:

for item in items:
    if item == "stop":
        break
    process(item)

continue пропускает остаток итерации и переходит к следующей:

for item in items:
    if item.startswith("_"):
        continue   # пропускаем приватные
    process(item)

Оба работают только во внутреннем цикле. Для выхода из вложенных - либо вынеси во функцию (с return), либо используй флаг.

for-else - редкая, но полезная идиома

После цикла можно поставить else, который выполняется если цикл завершился нормально (без break):

for item in items:
    if item == "needle":
        print("найдено")
        break
else:
    print("не найдено")

Это удобно для поиска в коллекции. Альтернатива - флаг:

found = False
for item in items:
    if item == "needle":
        found = True
        break
if not found:
    print("не найдено")

For-else короче, но иногда сбивает с толку. Используй где это явно лучше читается.

<InfoCard type="warning" title="else тут не "иначе""> Семантика else в for-else неинтуитивна: это «когда цикл прошёл до конца без break». В сообществе Python иногда предлагают переименовать в nobreak, но это уже исторически устоявшаяся часть языка.

Итерация по dict

data = {"name": "Alice", "age": 30, "city": "Moscow"}

# По ключам (по умолчанию)
for key in data:
    print(key)

# Явно по ключам
for key in data.keys():
    print(key)

# По значениям
for value in data.values():
    print(value)

# По парам
for key, value in data.items():
    print(f"{key} = {value}")

.items() - идиоматичный способ итерации по dict с распаковкой.

Распаковка в цикле

Когда элементы - кортежи или списки, можно распаковать в цикле:

points = [(1, 2), (3, 4), (5, 6)]
for x, y in points:
    print(f"x={x}, y={y}")

# С enumerate
for i, (x, y) in enumerate(points):
    print(f"{i}: ({x}, {y})")

Скобки вокруг (x, y) важны - иначе Python подумает что это три переменные i, x, y.

Типичные ошибки

1. Модификация коллекции во время итерации.

items = [1, 2, 3, 4, 5]
for item in items:
    if item % 2 == 0:
        items.remove(item)  # ВАЖНО: модифицировать во время итерации - undefined

print(items)  # [1, 3, 5] - но не всегда корректно

Правильно - создать новый список:

items = [item for item in items if item % 2 != 0]
# или
filtered = [item for item in items if item % 2 != 0]

2. Создание захватов в замыкании.

funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2] - не [0, 1, 2]!

Все лямбды захватили одну и ту же переменную i, и после цикла i = 2. Это классический late binding в замыканиях. Исправление:

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # default argument фиксирует значение

print([f() for f in funcs])  # [0, 1, 2]

3. Перебор файла дважды.

with open("file.txt") as f:
    for line in f:
        process(line)
    for line in f:  # ничего не выведет!
        print(line)

(Про сам with - в уроке про контекстные менеджеры.)

Файл-объект - итератор, после первого прохода он «выработан». Чтобы перебрать ещё раз - f.seek(0) или открыть заново.

Идиомы

Поиск элемента:

# Через for-else
for user in users:
    if user.email == target_email:
        found = user
        break
else:
    found = None

# Через next с генератором (короче)
found = next((u for u in users if u.email == target_email), None)

Все или ничего:

# all - все truthy
if all(user.is_active for user in users):
    notify_all(users)

# any - хотя бы один truthy
if any(user.is_admin for user in users):
    log_admin_present()

Дополнение списка в цикле:

# Плохо
result = []
for x in numbers:
    result.append(x * 2)

# Лучше - list comprehension
result = [x * 2 for x in numbers]

Про этот синтаксис - в уроке про comprehensions.

Мини-задание

  1. Перебери список с индексами:
words = ["alpha", "beta", "gamma", "delta"]

# Выведи: "1. alpha", "2. beta", ...
for i, word in enumerate(words, start=1):
    print(f"{i}. {word}")
  1. Параллельная итерация:
cities = ["Moscow", "London", "Paris"]
populations = [12_000_000, 9_000_000, 2_000_000]

for city, pop in zip(cities, populations):
    print(f"{city}: {pop:,}")
  1. Поиск через for-else:
numbers = [1, 5, 8, 13, 21]

for n in numbers:
    if n % 2 == 0:
        print(f"Первое чётное: {n}")
        break
else:
    print("Чётных нет")
  1. Найди ошибку - почему результат [2, 2, 2]?
multipliers = []
for x in [1, 2, 3]:
    multipliers.append(lambda n: n * x)

# Что выведет?
print([m(10) for m in multipliers])

(Решение: добавь x=x в lambda для фиксации значения.)

Что дальше

Освоили циклы. В следующем уроке - match-case: structural pattern matching, появившийся в Python 3.10. Это намного больше чем switch, но и со своими нюансами.

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