Время прочтения: 6 мин.

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

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

Lambda-выражения

Перед тем, как перейти непосредственно к функциям map(), filter(), reduce(), рассмотрим такую вспомогательную конструкцию, как лямбда-выражения. Лямбда-выражения являются анонимными функциями, которые выполняют только одно логическое выражение. Ниже приведен синтаксис:

lambda [arg1, arg2, ...]: [expression]

В качестве примера рассмотрим сортировку списка, который содержит кортежи с именем человека и его ростом. Отсортируем список по росту людей:

# Функция принимает на вход кортеж с именем и ростом, возвращает только рост
def get_height(human_tuple):
    height = human_tuple[1]
    return height

people = [('John', 155), ('Pete', 160), ('Clara', 188), ('Alex', 180)]
print(sorted(people, key=get_height))

Результат работы: [(‘John’, 155), (‘Pete’, 160), (‘Alex’, 180), (‘Clara’, 188)]

Теперь выполним ту же сортировку, но с пользованием лямбда-выражений.

people = [('John', 155), ('Pete', 160), ('Clara', 188), ('Alex', 180)]
print(sorted(people, key=lambda x: x[1]))

Результат работы: [(‘John’, 155), (‘Pete’, 160), (‘Alex’, 180), (‘Clara’, 188)]

Использование лямбда-выражений может быть полезным в нескольких случаях:

  1. когда есть только одно выражение для выполнения в функции;
  2. когда выполнить код нужно только один раз.

Что такое функции map(), filter() и reduce()

Функция map() принимает в качестве аргументов функцию и итерируемую последовательность. Позволяет применить переданную функцию к каждому элементу переданной итерируемой последовательности. Ниже приведен синтаксис:

map(function, iterable)

Функция filter() принимает в качестве аргументов функцию и итерируемую последовательность. Позволяет создать список из элементов переданной итерируемой последовательности, для которых переданная функция вернет True. Ниже приведен синтаксис:

filter(function, iterable)

Функция reduce() принимает в качестве аргументов функцию и итерируемую последовательность. Кумулятивно применяет переданную функцию к элементам итерируемой последовательности, сводя ее к единственному значению. Ниже приведен синтаксис:

reduce(function, iterable)

Начиная с Python версии 3.0 данная функция была перенесена в стандартный модуль functools.

Для каждой из перечисленных функций в качестве function можно использовать лямбда-выражения. Например:

# Получение квадратов чисел из списка [1, 2, 3, 4, 5]
map(lambda x: x**2, [1, 2, 3, 4, 5])

Списковые включения и генераторы списков

Списковые включения (List comprehensions) – встроенный в Python механизм генерации списков. Списковые включения позволяют строить списки из любых итерируемых объектов. Ниже приведен синтаксис:

# Без условий
[smt for smt in iterable]
# При наличии if
[smt for smt in iterable if condition]
# При наличии if/else
[smt if condition else other for smt in iterable]

Также существую словарные включения (Dictionary comprehensions) и включения множеств (Set comrehensions), которые работают полностью аналогично.

Генераторы списков (List generators) – также, как и списковые включения, является встроенным в Python механизмом генерации списков. Главным отличием от списковых включений является то, что в результате работы генератора списков возвращается объект типа generator. Ниже приведен синтаксис:

# Без условий
(smt for smt in iterable)
# При наличии if
(smt for smt in iterable if condition)
# При наличии if/else
(smt if condition else other for smt in iterable)

Альтернатива циклов for

Рассмотрим, как можно заменить циклы for с помощью функций map(), filter() и reduce().

Возьмем матрицу, представленную в виде списков:

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

Для демонстрации функции map() уменьшим каждый элемент матрицы на 30%:

# С использованием цикла for
result = []
for row in matrix:
    for item in row:
        item *= 0.3

# С использованием map
result = map(lambda x: x * 0.3, matrix)
# С использованием спискового включения
result = [item * 0.3 for row in matrix for item in row]

Для демонстрации функции filter() получим из матрицы все строки, сумма элементов которых меньше 10:

# С использованием цикла for
result = []
summ = 0
for row in matrix:
    summ = sum(row)
    if summ < 10:
        result.append(row)

# С использованием filter
result = filter(lambda x: sum(x) < 10, matrix)
# С использованием спискового включения
result = [row for row in matrix if sum(row) < 10]

Рассмотрим использование функции reduce() на примере приведение двумерной матрицы к одномерному виду:

# С использованием цикла for
result = []
for row in matrix:
    for item in row:
        result.append(item)

# С использованием reduce
result = reduce(lambda x, y: x + y, matrix)
# С использованием спискового включения
result = [item for row in matrix for item in row]

В данном примере списковое включение работает на порядок быстрее функции reduce(), однако также является и гораздо более сложным для понимания.

Использование генераторов для больших наборов данных

Списковые включения и циклы for загружают все входные данные в память. Для списков небольшого или среднего размера это нормально. Однако, если размер входных данных слишком велик, компьютеру может не хватить ресурсов, для создания такого большого списка и хранения его в памяти. В таком случае следует использовать генераторы.

Генераторы не хранят в памяти сразу все значения, вместо этого они хранят в памяти способ, по которому может быть получен следующий элемент последовательности, а также текущее состояние переменных генератора.

Рассмотренные в данной статье map(), filter() и генераторы списков в результате работы возвращают объект типа generator.

Сравнение производительности

Замерять время выполнения будем с использованием стандартной библиотеки timeit.

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

TAX = .3
transactions = [random.randrange(10000) for _ in range(1000000)]

# map с выносной функцией 
np.mean(timeit.repeat(map_func, repeat=1000, number=1))
# map с лямбда-выражением
np.mean(timeit.repeat(map_lambda, repeat=1000, number=1))
# генератор списков
np.mean(timeit.repeat(list_gen, repeat=1000, number=1))
# списковое включение
np.mean(timeit.repeat(list_comp, repeat=1000, number=1))
# генератор списков, приведенный к списку
np.mean(timeit.repeat(list_gen_to_list, repeat=1000, number=1))
# map с выносной функцией, пиведенный к списку
np.mean(timeit.repeat(map_func_to_list, repeat=1000, number=1))
# map с лямбда-выражением, приведенный к списку
np.mean(timeit.repeat(map_lambda_to_list, repeat=1000, number=1))
# цикл for
np.mean(timeit.repeat(for_loop, repeat=1000, number=1))

Результаты времени работы представлены в таблице:

ПодходВремя выполнения, мс
map/filter(func)2.25e-05
map/filter(lambda)2.40e-05
List generator3.25e-05
List comprehension(выражение)1.0795371
list(List generator)1.3387516
list(map/filter(func))1.5762654
list(map/filter(lambda))1.6126549
List comprehension(func)1.7002037
Цикл for2.172257

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

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

Ниже приведено изображение, результирующее сравнение производительности рассмотренных подходов: