История вопроса
Ленивая обработка (lazy evaluation) — это техника программирования, когда вычисления откладываются до тех пор, пока результат не понадобится этому коду. В языке Python эта парадигма приобрела популярность благодаря генераторам и специальным функциям стандартной библиотеки, таким как itertools. Изначально такие техники пришли из функциональных языков, но Python предоставляет свои нативные и сторонние инструменты для ленивой обработки.
Проблема
Традиционная eager (жадная) обработка требует загрузки и вычисления всех данных сразу (например, при использовании списковых выражений), что может приводить к большим затратам памяти и снижению производительности при работе с большими или бесконечными последовательностями. Ленивая обработка позволяет «подгружать» и обрабатывать элементы только по мере необходимости, избегая ненужных затрат ресурсов.
Решение
В Python ленивая обработка реализуется не только с помощью генераторов (yield), но и посредством специальных ленивых функций в модулях itertools, стандартных функций типа map, filter, а также объектов типа генераторных выражений (generator expressions). Например, функция map() возвращает ленивый итератор, который вычисляет значения только по мере обращения:
# Пример ленивой обработки: возведение каждого числа в квадрат squares = map(lambda x: x ** 2, range(10**10)) # не тратится память на список print(next(squares)) # 0 print(next(squares)) # 1
Ключевые особенности:
Всегда ли функция map() в Python реализует ленивую обработку? Как узнать, какие стандартные функции ленивы?
Нет, начиная с Python 3, функции map(), filter(), zip() возвращают итераторы, то есть реализуют ленивую обработку. В Python 2 эти функции возвращали списки. Чтобы узнать, ленив ли объект, нужно посмотреть его тип или изучить документацию:
result = map(lambda x: x+1, range(5)) print(type(result)) # 'map' — это итератор
Сработает ли ленивая обработка при применении генераторного выражения внутри функции sum()?
Функция sum() требует пройти по всему итератору до конца. Генераторное выражение само по себе ленивое, но sum() в итоге всё равно потребляет всю последовательность:
s = sum(x**2 for x in range(1000000)) # генератор расходуется полностью
Можно ли применить ленивая обработку к обычным спискам и кортежам, например, через map/lambda?
Да, можно, но списки и кортежи всё равно загружены в памяти. Map будет возвращать ленивый итератор по им, но исходные данные всё равно целиком в памяти. Для полноценной ленивой цепочки желательно работать с генераторами на каждом этапе:
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # всё лениво
Разработчик пишет обработку для большого файла логов, используя генераторное выражение для фильтрации строк, но случайно сразу преобразует его в список:
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # загружает весь файл
Плюсы:
Минусы:
Другая команда использует ленивый подход с генераторным выражением и обрабатывает строки по мере получения:
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
Плюсы:
Минусы: