Циклы: 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).
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.
Мини-задание
- Перебери список с индексами:
words = ["alpha", "beta", "gamma", "delta"]
# Выведи: "1. alpha", "2. beta", ...
for i, word in enumerate(words, start=1):
print(f"{i}. {word}")
- Параллельная итерация:
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:,}")
- Поиск через for-else:
numbers = [1, 5, 8, 13, 21]
for n in numbers:
if n % 2 == 0:
print(f"Первое чётное: {n}")
break
else:
print("Чётных нет")
- Найди ошибку - почему результат
[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 для фиксации значения.)
Что дальше
- Go - Цикл for: все варианты + типичные ошибки - как те же break/continue, бесконечный цикл и range реализованы в Go
Освоили циклы. В следующем уроке - match-case: structural pattern matching, появившийся в Python 3.10. Это намного больше чем switch, но и со своими нюансами.