惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
小众软件
小众软件
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - Franky
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
Jina AI
Jina AI
博客园 - 聂微东
S
SegmentFault 最新的问题
量子位
宝玉的分享
宝玉的分享
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园_首页

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Разбираем map, filter, reduce, any, all, zip и enumerate ...
enamored_poc · 2026-05-19 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение13 мин

Охват и читатели10

Туториал

Все мы начинали писать на Python примерно одинаково: создавали пустой список, запускали цикл for, проверяли условие через if и делали .append(). Это надежно, предсказуемо, но слишком громоздко. По мере роста навыка и объема кодовой базы такие конструкции начинают утомлять — мы тратим 4-5 строк кода на банальную трансформацию данных, которую можно уложить в одну лаконичную строку.

В этой статье мы разберем встроенный инструментарий Python для работы с итерируемыми объектами: map, filter, reduce, any, all, zip и enumerate.

Но мы не будем просто цитировать официальную документацию и показывать скучный синтаксис. Цель этого материала — разобраться, как эти инструменты работают «под капотом».

О чем конкретно поговорим:

  • Что такое ленивые вычисления (lazy evaluation) и как эти функции экономят оперативную память на гигантских объемах данных.

  • Как работает short-circuiting (короткое замыкание) в логических редукторах.

  • Почему reduce выкинули из встроенных функций в модуль functools.

  • Бенчмарки: замерим timeit и поставим точку в споре о том, когда использовать функциональщину, а когда старые добрые генераторы списков (List/Generator Comprehensions).

Часть 1. Логические редукторы: any() и all()

Давайте начистоту: все мы когда-то писали подобный код, чтобы проверить, есть ли в массиве хотя бы один подходящий элемент:

def has_even_numbers(numbers: list[int]) -> bool:
    has_even = False
    for num in numbers:
        if num % 2 == 0:
            has_even = True
            break
    return has_even

Пять строк кода ради элементарной проверки. В Python для этого есть встроенные функции any() и all(), которые сводят эту логику к одной читаемой строке и работают на уровне языка C.

Как это работает: логическое И / ИЛИ

Всё сводится к базовой булевой алгебре:

  • all(iterable) — это логическое И (AND). Возвращает True только если все элементы последовательности истинны (в терминологии Python — «truthy»). Важный нюанс: если передать пустой объект, all() вернет True.

  • any(iterable) — это логическое ИЛИ (OR). Возвращает True, если хотя бы один элемент истинен. Для пустого объекта вернет False.

Они избавляют нас от необходимости вручную объявлять флаги-переменные и писать блоки if/break.

«Ленивость» (Short-circuiting): главная фишка

Самая частая ошибка при использовании any и all — думать, что они сначала перебирают весь массив, а потом выдают результат. На самом деле они используют короткое замыкание (short-circuit evaluation).

  • Как только any() встречает первый True, он мгновенно останавливает итерацию и возвращает True.

  • Как только all() встречает первый False, он тут же прерывает цикл и возвращает False.

Но здесь есть критически важная ловушка производительности: чтобы короткое замыкание сработало и сэкономило CPU и память, внутрь нужно передавать генераторное выражение, а не генератор списка (List Comprehension).

Смотрите на разницу:

# ❌ ПЛОХО: Сначала в оперативной памяти создастся список из миллиона элементов,
# цикл пройдет до конца, и только потом any() посмотрит на первый элемент.
result = any([x == 0 for x in range(1_000_000)])

# ✅ ОТЛИЧНО: Передаем генератор (обратите внимание на отсутствие квадратных скобок).
# any() попросит первый элемент, получит True (так как 0 == 0) и моментально остановит работу.
# Остальные 999 999 итераций даже не будут вычислены.
result = any(x == 0 for x in range(1_000_000))

Практический пример: Валидация данных

Где это реально используется? Идеальный кейс — валидация форм, паролей или конфигурационных файлов.

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

def is_strong_password(password: str) -> bool:
    # Проверяем условия лениво, останавливаясь при первом же совпадении
    has_digit = any(char.isdigit() for char in password)
    has_upper = any(char.isupper() for char in password)
    
    return has_digit and has_upper

print(is_strong_password("Qwerty123"))  # True

Пример 2: Проверка заполненности полей Проверим, что все обязательные поля в словаре, пришедшем от пользователя, заполнены (то есть не равны None, пустой строке, пустому списку или нулю).

user_form = {
    "username": "admin",
    "email": "admin@example.com",
    "age": 25,
    "bio": ""  # Пустая строка расценивается Python как False
}

# all() дойдет до ключа "bio", получит False и сразу прервет проверку
if not all(user_form.values()):
    print("Ошибка: Заполните все поля корректно!")

Использование any и all делает код декларативным: вы описываете, что хотите получить, перекладывая заботу о том, как именно управлять циклом и когда делать break, на встроенные механизмы Python.

Часть 2. Троица функционального программирования

1. Трансформация: map()

Смысл map заложен в его названии: он проецирует (маппит) заданную функцию на каждый элемент коллекции. Это классическая замена циклу, внутри которого происходит какое-то вычисление и добавление результата в новый список.

Базовый синтаксис: map(function, iterable)

Первым аргументом идет ссылка на функцию (без скобок вызова), вторым — объект, по которому будем итерироваться.

# Быстрый парсинг строк в числа
strings = ["10", "20", "30"]
numbers = map(int, strings)

Как работает в Python 3: Итераторы и экономия памяти

Если вы запустите код выше и сделаете print(numbers), то вместо ожидаемого списка [10, 20, 30] увидите что-то вроде <map object at 0x7f8b...>. И это лучшее, что произошло с map при переходе с Python 2 на Python 3.

В старом добром Python 2 map возвращал готовый список. Если вы скармливали ему массив на 10 миллионов строк, он добросовестно выделял гигабайты оперативной памяти, чтобы вернуть вам новый список на 10 миллионов чисел.

В Python 3 map возвращает итератор. Он не вычисляет все значения разом. Вместо этого он выдает результат трансформации строго по одному элементу в тот момент, когда вы его об этом просите (например, при обходе в цикле for или при оборачивании в list()).

С точки зрения памяти, map занимает O(1) — то есть считанные байты, независимо от того, обрабатываете вы список из трех элементов или читаете лог-файл на 50 гигабайт.

Продвинутый трюк: несколько итерируемых объектов

Часто разработчики забывают, что map умеет принимать более одного итерируемого объекта, если переданная функция требует нескольких аргументов. Он будет брать элементы из всех переданных коллекций параллельно.

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

import operator

prices = [1000, 2500, 3200]
taxes = [150, 350, 480]
discounts = [50, 100, 200]

# Функция operator.add принимает два аргумента, 
# но мы можем передать и лямбду для трех аргументов:
final_prices = list(map(lambda p, t, d: p + t - d, prices, taxes, discounts))

print(final_prices) # [1100, 2750, 3480]

Важный нюанс: если длины коллекций не совпадают, map остановится, как только закончится самая короткая из них (точно так же работает zip).

Пример «под капотом»: пишем свой map

Чтобы окончательно убрать магию и понять, как работает ленивое вычисление в map, давайте напишем его аналог на чистом Python с использованием генератора (yield).

def my_map(func, *iterables):
    # zip параллельно собирает элементы из всех переданных коллекций
    for args in zip(*iterables):
        # Вычисляем и отдаем результат только тогда, когда его запросили
        yield func(*args)

# Проверяем наш велосипед
prices = [100, 200]
taxes = [15, 30]

lazy_result = my_map(operator.add, prices, taxes)

print(next(lazy_result)) # 115 (вычислился только первый элемент)
print(next(lazy_result)) # 230 (вычислился только второй)

Именно ключевое слово yield делает функцию генератором. Вызов func(*args) происходит не для всего массива сразу, а только в момент вызова next() (который неявно происходит под капотом функции list() или цикла for). Это и есть суть ленивых вычислений.

2. Фильтрация: filter()

Если map трансформирует данные, то filter отсекает мусор. По механике работы в Python 3 он абсолютно идентичен map: не создает новых списков в памяти, а возвращает ленивый итератор, потребляя O(1) памяти независимо от объемов обрабатываемого потока.

Базовый синтаксис: filter(function, iterable)

Первым аргументом идет функция-предикат. Она должна возвращать True (или любое «truthy» значение) для элементов, которые нужно пропустить дальше, и False (или «falsy») для тех, что идут под нож.

# Фильтруем логи: оставляем только ошибки
logs = ["INFO: start", "ERROR: db crash", "DEBUG: init", "ERROR: timeout"]
errors = filter(lambda line: line.startswith("ERROR"), logs)

С None (Быстрая очистка данных)

Самый частый, но почему-то редко упоминаемый в туториалах юзкейс filter — передача None вместо функции-предиката.

Если передать None, filter не будет вызывать никаких функций, а просто проверит сами элементы на истинность. Он безжалостно выкосит из коллекции всё, что в Python неявно приводится к лжи: 0, False, пустые строки "", пустые списки [], словари {} и, разумеется, сам None.

Это идеальный инструмент для очистки «грязных» данных. Например, когда вы распарсили кривой CSV или получили ответ от нестабильного API:

# На входе месиво из данных
dirty_data = ["apple", "", None, "banana", 0, False, "cherry", []]

# В одну строчку очищаем массив
clean_data = list(filter(None, dirty_data))

print(clean_data) 
# Вывод: ['apple', 'banana', 'cherry']

Согласитесь, это выглядит гораздо элегантнее, чем громоздкое [x for x in dirty_data if x]. Но раз уж мы заговорили про генераторы…

Холивар: filter() против генераторов списков (List Comprehensions)

Что использовать: filter или if внутри list comprehension? Ответ кроется в архитектуре интерпретатора CPython. В Python вызов любой функции (overhead на создание фрейма стека) — относительно дорогая операция.

Правило большого пальца:

  1. Если вам нужна lambda — пишите генератор. Вызов лямбды на каждой итерации внутри filter сожрет всё время. Генератор списка не вызывает дополнительных функций, он оценивает выражение на месте.

# ❌ Медленнее и хуже читается
evens = filter(lambda x: x % 2 == 0, numbers)

# ✅ БЫСТРЕЕ И ЧИТАЕМЕЕ (выбор PEP 8)
evens = [x for x in numbers if x % 2 == 0] 

  1. Если вы используете встроенную функцию C-уровня (built-in) — берите filter. Если предикатом выступает метод строк (вроде str.isdigit или str.isupper), встроенная функция или None, filter порвет генератор списков по производительности. Почему? Потому что весь цикл фильтрации крутится на уровне языка C, вообще не обращаясь к виртуальной машине Python для оценки выражений.

    strings = ["123", "a", "45", "b"]

    # ✅ БЫСТРЕЕ (цикл идет на уровне C)
    numbers = filter(str.isdigit, strings) 

    # ❌ Чуть медленнее (выполняется байткод Python для каждой итерации)
    numbers = [s for s in strings if s.isdigit()] 
    ```

Вывод простой: `filter` идеален в связке с `None` или готовыми Сишными функциями. Во всех остальных случаях сложной логики Дзен Питона и бенчмарки подсказывают использовать генераторы.

3. Аккумуляция: reduce()

Если map и filter обрабатывают элементы независимо друг от друга, то reduce заставляет их взаимодействовать, схлопывая (редуцируя) всю коллекцию в одно итоговое значение.

Историческая справка: Изгнание в functools

В Python 2 функция reduce была встроенной (built-in), как map и filter. Но при переходе на Python 3 Гвидо ван Россум принял волевое решение убрать её из базового неймспейса и спрятать в модуль functools.

Почему? Гвидо считал, что в 99% случаев использование reduce делает код нечитаемым, и обычный цикл for с переменной-аккумулятором понятнее любому разработчику, который будет поддерживать код после вас. Исключение составляли лишь самые тривиальные операции вроде суммирования (для которого в итоге сделали встроенную функцию sum()). Тем не менее, reduce остался мощным инструментом в арсенале функционального программиста.

Механика работы: пошаговый разбор

Синтаксис: reduce(function, iterable, [initial])

Функция, которую вы передаете в reduce, должна обязательно принимать два аргумента:

  1. Аккумулятор (acc) — хранит промежуточный результат вычислений.

  2. Текущее значение (x) — очередной элемент из коллекции.

Давайте посмотрим на процесс перемножения элементов списка [1, 2, 3, 4].

from functools import reduce

numbers = [1, 2, 3, 4]
result = reduce(lambda acc, x: acc * x, numbers)

Что происходит под капотом (Таблица состояний):

Шаг

Аккумулятор (acc)

Текущий элемент (x)

Вычисление (acc * x)

Новый Аккумулятор

1

1 (первый элемент)

2 (второй элемент)

1 * 2

2

2

2

3 (третий элемент)

2 * 3

6

3

6

4 (четвертый элемент)

6 * 4

24 (Итог)

Обратите внимание: на первом шаге reduce берет сразу два первых элемента последовательности.

Начальное значение (initial): Спасение от TypeError

Что будет, если передать в reduce пустой список?

# ❌ Упадет с TypeError: reduce() of empty sequence with no initial value
reduce(lambda acc, x: acc * x, []) 

Чтобы код не падал в рантайме на непредсказуемых данных, у reduce есть третий, опциональный аргумент — initial (начальное значение). Если он задан, reduce использует его в качестве аккумулятора на самом первом шаге, а x становится первым элементом списка.

# ✅ Вернет 1 (начальное значение), так как список пуст
reduce(lambda acc, x: acc * x, [], 1) 

Это правило хорошего тона: если вы не уверены на 100%, что коллекция не пуста, всегда передавайте initial. Для сложения это 0, для умножения — 1, для конкатенации списков — [].

Сильные связки: reduce + operator

Настоящая магия reduce раскрывается в связке со встроенным модулем operator, который содержит Си-реализации стандартных математических и логических операций. Это позволяет избавиться от медленных лямбд.

1. Перемножение всех чисел массива (аналог sum, но для умножения):

import operator
from functools import reduce

numbers = [1, 2, 3, 4, 5]
# Работает быстрее, чем lambda acc, x: acc * x
product = reduce(operator.mul, numbers, 1) 

2. Глубокий поиск в словаре (Deep dict lookup):

Частая задача: есть сложный вложенный JSON/словарь, и есть путь к нужному ключу в виде списка. Писать рекурсию или цикл for? Можно обойтись одной строкой.

import operator
from functools import reduce

data = {
    "user": {
        "profile": {
            "settings": {
                "theme": "dark"
            }
        }
    }
}

path = ["user", "profile", "settings", "theme"]

# operator.getitem(a, b) делает то же самое, что a[b]
theme = reduce(operator.getitem, path, data)

print(theme) # Вывод: 'dark'

В этом примере reduce последовательно “проваливается” вглубь словаря: data["user"] -> ["profile"] -> ["settings"] -> ["theme"]. Это один из самых элегантных и практичных паттернов использования reduce в реальном продакшн-коде.

Часть 3. Верные помощники: zip() и enumerate()

Часто, даже зная про map и for, разработчики начинают городить костыли, когда нужно получить доступ к индексу элемента или обойти несколько коллекций одновременно. В Python для этого есть два мощных инструмента, которые также работают по принципу ленивых вычислений (возвращают итераторы).

enumerate(): Прощай, i = 0 и i += 1

Один из главных признаков человека, который недавно перешел в Python из C++ или Java — это итерация по индексам через range(len(...)).

# ❌ ПЛОХО: Не-Pythonic стиль (C-style)
users = ["Alice", "Bob", "Charlie"]
for i in range(len(users)):
    print(f"ID {i}: {users[i]}")

# ❌ ПЛОХО: Ручной счетчик
i = 0
for user in users:
    print(f"ID {i}: {user}")
    i += 1

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

# ✅ ОТЛИЧНО: Идиоматичный Python
users = ["Alice", "Bob", "Charlie"]
for i, user in enumerate(users):
    print(f"ID {i}: {user}")

Полезный трюк: У enumerate есть второй, малоизвестный новичкам аргумент start. Он позволяет задать начальное значение счетчика. Это невероятно удобно, если вам нужно вывести список для пользователя (где нумерация обычно начинается с 1, а не с 0):

for rank, user in enumerate(users, start=1):
    print(f"Место #{rank}: {user}")

zip(): Параллельная итерация

Представьте, что у вас есть два или более списка связанных данных, и вам нужно пройтись по ним одновременно. Опять же, рука тянется написать цикл по индексам. И снова это плохая идея.

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

names = ["Alice", "Bob", "Charlie"]
salaries = [150_000, 180_000, 200_000]

# ✅ Элегантная параллельная итерация
for name, salary in zip(names, salaries):
    print(f"{name} зарабатывает {salary}")

Помимо циклов, zip() — это самый быстрый способ создать словарь из двух списков:

user_salaries = dict(zip(names, salaries))

Критически важный нюанс: обрезка данных

В работе zip() есть одна ловушка, о которую разбиваются многие дата-пайплайны. zip всегда останавливается по самому короткому итерируемому объекту.

Если в одном списке 100 элементов, а во втором 99, zip молча отбросит сотый элемент первого списка, и вы об этом даже не узнаете (никаких исключений выброшено не будет).

names = ["Alice", "Bob", "Charlie", "David"] # 4 имени
salaries = [150_000, 180_000]                # 2 зарплаты

# David и Charlie просто исчезнут из результата
print(list(zip(names, salaries))) 
# [('Alice', 150000), ('Bob', 180000)]

Спасение: itertools.zip_longest()

Если потеря данных недопустима и вам нужно сохранить все элементы из самого длинного списка, на помощь приходит стандартная библиотека itertools и функция zip_longest().

Вместо обрезки она заполняет недостающие значения с помощью параметра fillvalue (по умолчанию None).

from itertools import zip_longest

names = ["Alice", "Bob", "Charlie"]
salaries = [150_000]

# Сохраняем всех, кому не хватило зарплаты, записываем 'N/A'
result = list(zip_longest(names, salaries, fillvalue="N/A"))

print(result)
# [('Alice', 150000), ('Bob', 'N/A'), ('Charlie', 'N/A')]

Бонус: Если вам нужно строго гарантировать, что длины списков совпадают, начиная с Python 3.10 у функции zip появился флаг strict=True. При zip(a, b, strict=True) интерпретатор выбросит ValueError, если длины окажутся разными. Это отличный способ защитить код от плавающих багов на этапе валидации данных.

Часть 4. Холиварная тема: Built-ins vs List Comprehensions (Генераторы списков)

В мире Python существует давний спор: что лучше использовать — функциональные встроенные функции (map, filter) или генераторы списков (List Comprehensions)?

Гвидо ван Россум всегда был сторонником второго подхода. Давайте разберем этот холивар с двух сторон: читаемость кода и сухие цифры бенчмарков.

Читаемость (Дзен Пайтона)

Дзен Питона гласит: «Flat is better than nested» (Плоское лучше, чем вложенное) и «Readability counts» (Читаемость имеет значение).

Представьте, что нам нужно взять массив чисел, оставить только четные и возвести их в квадрат. Сравните два подхода:

nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# ❌ Функциональный стиль (плохо читается)
result = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, nums)))

# ✅ Pythonic стиль (генератор списка)
result = [x**2 for x in nums if x % 2 == 0]

В первом случае вам приходится читать код «изнутри наружу», продираясь через скобки и визуальный шум от lambda. Во втором случае код читается как обычное предложение на английском языке: «Квадрат икса для каждого икса в массиве чисел, если икс четный».

С точки зрения поддержки кодовой базы, List Comprehensions безоговорочно выигрывают. Но что насчет производительности?

Бенчмарки производительности (Модуль timeit)

Давайте измерим скорость выполнения с помощью стандартного модуля timeit. Мы прогоним два типичных сценария по 1 000 итераций на массивах из 10 000 элементов.

Сценарий 1: Использование Сишных функций (Побеждает map)

Посмотрим, кто быстрее приведет список строк к нижнему регистру. В этом сценарии мы передаем в map метод str.lower, написанный на C.

import timeit

setup = "strings = ['Habr Python'] * 10000"

# Тестируем map
time_map = timeit.timeit(
    "list(map(str.lower, strings))", 
    setup=setup, number=1000
)

# Тестируем List Comprehension
time_comp = timeit.timeit(
    "[s.lower() for s in strings]", 
    setup=setup, number=1000
)

print(f"map: {time_map:.3f} сек.")
print(f"list comp: {time_comp:.3f} сек.")

Примерный результат вывода:

map: 0.420 сек.
list comp: 0.650 сек.

Почему map быстрее? Когда вы передаете встроенную функцию C-уровня в map, весь цикл прогона элементов выполняется на уровне языка C. Интерпретатору не нужно обращаться к байткоду Python на каждой итерации. В генераторе же выражение s.lower() вычисляется виртуальной машиной Python на каждом шаге, что добавляет заметный оверхед.

Сценарий 2: Использование кастомной логики через lambda (Побеждают Генераторы)

Теперь попробуем возвести числа в квадрат. Готовой Сишной функции для этого нет, поэтому для map придется использовать lambda.

setup_nums = "nums = list(range(10000))"

# Тестируем map + lambda
time_map_lambda = timeit.timeit(
    "list(map(lambda x: x**2, nums))", 
    setup=setup_nums, number=1000
)

# Тестируем List Comprehension
time_comp_math = timeit.timeit(
    "[x**2 for x in nums]", 
    setup=setup_nums, number=1000
)

print(f"map + lambda: {time_map_lambda:.3f} сек.")
print(f"list comp: {time_comp_math:.3f} сек.")

Примерный результат вывода:

map + lambda: 2.150 сек.
list comp: 1.820 сек.

Почему генератор быстрее? В Python вызов любой функции — это довольно дорогостоящая операция. Когда map вызывает lambda на каждой из 10 000 итераций, интерпретатор вынужден создавать новый фрейм стека для каждого вызова. Генератор списка не вызывает никаких дополнительных функций; он оценивает выражение x2 прямо по месту «прописки» (inline), избавляясь от накладных расходов на вызовы.

Краткое резюме (Чек-лист разработчика)

Чтобы больше не сомневаться на код-ревью, сохраните себе этот алгоритм принятия решений:

  1. Нужно быстро проверить наличие/отсутствие условий в массиве? Берем all() / any() с генераторным выражением внутри.

  2. Нужно применить к данным встроенную функцию (C-уровня) вроде len, int, str.lower? Используем map(). Это быстрее.

  3. Нужно очистить список от нулей, None и пустых строк? Используем filter(None, data).

  4. Нужно применить сложную кастомную логику, математику или отфильтровать по условию? Забываем про lambda и пишем List/Generator Comprehensions. Это читаемее и быстрее.

  5. Нужно схлопнуть весь массив данных в одно значение, опираясь на предыдущие результаты? Импортируем reduce().

Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.

Функциональный подход в Python — это мощный инструмент, который делает код элегантным и быстрым, но только если вы четко понимаете, что происходит под капотом виртуальной машины в момент вызова этих функций.