Python enumerate, zip, map и filter
Как работают встроенные функции Python enumerate, zip, map и filter: примеры и практические паттерны для написания чистого кода итерации.
Python включает четыре встроенные функции — enumerate, zip, map и filter — которые заменяют распространённые паттерны циклов одним читаемым выражением. Каждая из них возвращает итератор (ленивый, эффективно использующий память), и они естественно сочетаются с циклами for и генераторами списков.
В этой главе рассматривается:
enumerate()— цикл с автоматическим индексомzip()— параллельный обход двух и более последовательностейmap()— применение функции к каждому элементуfilter()— сохранение только элементов, прошедших проверку- Комбинирование всех четырёх функций в реальных паттернах
- Когда предпочесть генератор списков
enumerate() — индекс при обходе цикла
Когда во время цикла нужны и позиция, и значение, наивный подход использует ручной счётчик:
colors = ["red", "green", "blue"]
i = 0
for color in colors:
print(i, color)
i += 1enumerate() делает это автоматически. Он оборачивает любой итерируемый объект и возвращает пары (индекс, значение):
colors = ["red", "green", "blue"]
for i, color in enumerate(colors):
print(i, color)Вывод:
0 red
1 green
2 blueНачало с другого индекса
Передайте аргумент start, чтобы начать отсчёт с любого целого числа:
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")Вывод:
1. apple
2. banana
3. cherryЭто полезно при формировании нумерованных списков для отображения, где нумерация с 0 выглядит непривычно для читателя.
enumerate() с условием
Поскольку enumerate() — это обычный итератор, внутри тела цикла можно добавлять проверки if:
scores = [72, 91, 55, 88, 44]
for rank, score in enumerate(scores, start=1):
if score >= 80:
print(f"Rank {rank}: score {score} — PASS")Вывод:
Rank 2: score 91 — PASS
Rank 4: score 88 — PASSПолучение только индекса без enumerate()
Если нужен только индекс, подойдёт range(len(seq)) — но как только потребуется и значение, используйте enumerate(), чтобы не писать seq[i] повсюду.
zip() — параллельный обход нескольких последовательностей
zip() объединяет два и более итерируемых объекта в единый итератор кортежей, сопоставляя элементы по позициям:
names = ["Alice", "Bob", "Charlie"]
scores = [95, 87, 92]
for name, score in zip(names, scores):
print(f"{name}: {score}")Вывод:
Alice: 95
Bob: 87
Charlie: 92zip() с тремя и более итерируемыми объектами
first = ["a", "b", "c"]
second = [1, 2, 3]
third = [True, False, True]
for x, y, z in zip(first, second, third):
print(x, y, z)Вывод:
a 1 True
b 2 False
c 3 TrueПоследовательности разной длины
zip() останавливается на самой короткой последовательности и молча отбрасывает оставшиеся элементы из более длинных:
short = [1, 2]
long = [10, 20, 30, 40]
print(list(zip(short, long)))Вывод:
[(1, 10), (2, 20)]Значения 30 и 40 из long отбрасываются. Если нужно сохранить все элементы, используйте itertools.zip_longest из стандартной библиотеки:
from itertools import zip_longest
short = [1, 2]
long = [10, 20, 30, 40]
print(list(zip_longest(short, long, fillvalue=0)))Вывод:
[(1, 10), (2, 20), (0, 30), (0, 40)]Преобразование результата zip в список или словарь
zip() возвращает ленивый итератор. Оберните его в list(), чтобы сразу получить все пары, или передайте в dict(), чтобы создать словарь из двух параллельных последовательностей:
keys = ["name", "age", "city"]
values = ["Alice", 30, "Paris"]
record = dict(zip(keys, values))
print(record)Вывод:
{'name': 'Alice', 'age': 30, 'city': 'Paris'}Разупаковка с помощью zip(*...)
Трюк с оператором * «разворачивает» последовательность кортежей обратно в отдельные последовательности:
pairs = [(1, "a"), (2, "b"), (3, "c")]
numbers, letters = zip(*pairs)
print(numbers) # (1, 2, 3)
print(letters) # ('a', 'b', 'c')map() — применение функции к каждому элементу
map(function, iterable) применяет function к каждому элементу и возвращает ленивый итератор результатов. Он заменяет цикл for, который создаёт новый список, преобразуя каждый элемент.
numbers = [1, 2, 3, 4, 5]
squares = map(lambda x: x ** 2, numbers)
print(list(squares))Вывод:
[1, 4, 9, 16, 25]map() с именованной функцией
Можно передать любой вызываемый объект — встроенную функцию, функцию, определённую через def, или lambda:
words = [" hello ", " world ", " python "]
stripped = list(map(str.strip, words))
print(stripped)Вывод:
['hello', 'world', 'python']Здесь str.strip передаётся как ссылка (без скобок). map() вызывает str.strip(word) для каждого элемента.
map() с несколькими итерируемыми объектами
Передайте дополнительные итерируемые объекты для применения функции, принимающей два и более аргументов. map() проходит по всем объектам параллельно (как zip(), останавливается на самом коротком):
a = [1, 2, 3]
b = [10, 20, 30]
totals = list(map(lambda x, y: x + y, a, b))
print(totals)Вывод:
[11, 22, 33]map() против генератора списков
Оба подхода допустимы. Практическое правило:
| Ситуация | Предпочтительно |
|---|---|
| Применение существующей именованной функции | map() — короче, lambda не нужна |
| Написание встроенного выражения | Генератор списков — более читаемо |
| Несколько входных последовательностей параллельно | map() с несколькими итерируемыми объектами |
Эквивалентные формы:
# map() with a named function
result = list(map(str.upper, ["a", "b", "c"]))
# list comprehension — same result
result = [s.upper() for s in ["a", "b", "c"]]filter() — сохранение только элементов, соответствующих условию
filter(function, iterable) оставляет элементы, для которых function возвращает истинное значение. Возвращает ленивый итератор.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = filter(lambda x: x % 2 == 0, numbers)
print(list(evens))Вывод:
[2, 4, 6, 8, 10]filter() с именованной функцией
def is_positive(n):
return n > 0
values = [-3, -1, 0, 4, 7, -2]
positives = list(filter(is_positive, values))
print(positives)Вывод:
[4, 7]filter(None, iterable) — удаление ложных значений
Передача None в качестве функции сохраняет только истинные элементы. Это быстрый способ убрать пустые строки, нули и значения None:
mixed = [0, "hello", "", None, 42, False, "world"]
truthy = list(filter(None, mixed))
print(truthy)Вывод:
['hello', 42, 'world']filter() против генератора списков
filter() и условный генератор списков выполняют одну и ту же задачу:
numbers = range(1, 11)
# filter()
evens_f = list(filter(lambda x: x % 2 == 0, numbers))
# list comprehension — equivalent
evens_c = [x for x in numbers if x % 2 == 0]
print(evens_f) # [2, 4, 6, 8, 10]
print(evens_c) # [2, 4, 6, 8, 10]Предпочитайте генератор списков, когда условие написано встроенно; используйте filter(), когда у вас уже есть хорошо именованная функция-предикат.
Комбинирование enumerate, zip, map и filter
Эти четыре функции естественно сочетаются. Вот реалистичный пример: у вас есть два параллельных списка — имена студентов и их сырые баллы; нужно вывести пронумерованный отчёт, включающий только прошедших испытание (балл >= 60), с баллами, конвертированными в буквенные оценки.
def letter_grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
return "F"
names = ["Alice", "Bob", "Carol", "Dave", "Eve"]
scores = [85, 52, 91, 67, 48]
# Step 1: pair names with scores
pairs = zip(names, scores)
# Step 2: keep only passing students
passing = filter(lambda pair: pair[1] >= 60, pairs)
# Step 3: convert scores to letter grades
graded = map(lambda pair: (pair[0], letter_grade(pair[1])), passing)
# Step 4: number the results
for rank, (name, grade) in enumerate(graded, start=1):
print(f"{rank}. {name}: {grade}")Вывод:
1. Alice: B
2. Carol: A
3. Dave: DКаждый шаг создаёт итератор; ничто не материализуется в список до тех пор, пока цикл for не израсходует его. Это обеспечивает постоянный расход памяти независимо от размера входных данных.
Распространённые ошибки
Итераторы исчерпываются после одного прохода
map(), filter() и zip() возвращают итераторы, а не списки. После того как итератор потреблён, он пуст:
squares = map(lambda x: x ** 2, [1, 2, 3])
print(list(squares)) # [1, 4, 9]
print(list(squares)) # [] — already exhaustedОберните в list() заранее, если нужно обойти данные более одного раза.
zip() молча отбрасывает конечные элементы
Когда входные последовательности имеют разную длину, zip() отбрасывает лишние элементы без предупреждения. Явно проверяйте длины, если это важно, или используйте itertools.zip_longest.
enumerate() не изменяет итерируемый объект
enumerate() оборачивает исходный итерируемый объект и не копирует его. Изменение базового списка в процессе итерации может привести к неожиданным результатам — та же осторожность, что и при обычных циклах for.
Краткий справочник
| Функция | Сигнатура | Что возвращает |
|---|---|---|
enumerate | enumerate(iterable, start=0) | Итератор кортежей (index, value) |
zip | zip(*iterables) | Итератор кортежей, по одному элементу из каждого итерируемого объекта |
map | map(function, *iterables) | Итератор результатов применения function к каждому элементу |
filter | filter(function, iterable) | Итератор элементов, для которых function возвращает True |
Все четыре функции ленивы: они производят значения по требованию, не создавая полный список заранее.