python
March 19, 2021

Flat map в Python 🐍

Некоторое время назад мы касались вопросов функционального программирования на Swift в контексте монад и функторов (flatMap там тоже был). Теперь давайте рассмотрим концепцию flat_map в языке Python.

Ну, здесь нет built-in функции flat_map. Но её можно имплементировать самостоятельно, причём несколькими разными способами. Поэтому, вместо того чтобы дискутировать на тему на создания «pure pythonic» функции flat_map, давайте лучше рассмотрим более интересный вопрос:

Как создать функцию flat_map в python наиболее эффективным способом?

Определение

Начнём с определения. flat_map это операция, которая принимает список элементов типа A и функцию f типа A -> [B]. Функция f таким образом применяется ко всем элементам первоначального списка, и затем все результаты конкатенируются. Простой пример:

Хотя данная операция довольно простая, идея, лежащая в её основе называется «монада». Насколько я знаю, есть 4 простых способа создать flat_map в Python:

  • цикл for и extend
  • двойной list comprehension
  • map и reduce
  • map и sum

Измерение

Какой подход будет наиболее эффективным? Чтобы ответить на этот вопрос, я собираюсь проверить время исполнения программы в 3 различных случаях:

a) 100 списков, каждый содержит 10 чисел типа int
b) 10 000 списков, каждый содержит 10 чисел типа int
c) 10 000 списков, каждый содержит 10 объектов (инстансов класса)

Чтобы сделать это, я буду использовать функцию check_time и дополнительную простую функцию id для её передачи в flat_map:

Весь используемый код можно найти по ссылке.

Map reduce

Наиболее «функциональный» ответ на поставленную задачу – это комбинация map и reduce. Потребуется импорт библиотеки functools (документация), так что даже при всей его крутизне, это всё таки не решение в одну строчку.

Результат:
a) 0.1230 мс
b) 1202.4405 мс
c) 3249.0119 мс

Тут map можно легко заменить на list comprehension, но особой разницы заметно не будет.

Map sum

Другое «функциональное» решение – это использовать sum. Мне нравится этот подход, но его производительность также оставляет желать лучшего. Результаты в принципе не лучше, чем в предыдущем случае.

Результат:
a) 0.1080 мс
b) 1150.4400 мс
c) 3296.3167 мс

For extend

До сих пор наши имплементации функции flat_map работали относительно быстро для малого размера списка, и были медленными для большого. Однако данная «классическая» имплементация в 1000 раз быстрее! Жаль конечно что это не решение в одну строчку. Но, наверное, это относительно небольшая цена за такую производительность.

Результат:
a) 0.0211 мс
b) 2.4535 мс
c) 2.7347 мс

Интересно то, что разница между скоростью работы flat_map в случае со списком интов и списком классов теперь не такая уж и большая.

List comprehension

Настоящий «pythonic» подход (и в принципе тоже функциональный). Единственный минус данной реализации, это слегка запутанный двойной for цикл.

Результат:
a) 0.0372 мс
b) 4.1477 мс
c) 4.5945 мс

Как мы видим, производительность действительно потрясающая. Немного медленнее, чем в предыдущем варианте, но теперь то это настоящее решение в одну строчку.

Давайте посмотрим что будет если мы заменим list comprehension на generator expression в последнем примере:

Результат:
a) 0.0008 мс
b) 0.0008 мс
c) 0.0007 мс

Так вот он наш победитель? А стоп, мы не развернули само выражение. Давайте развернем, чтобы проверить его реальную производительность. Кроме того, развернем и list comprehension, для того, чтобы посмотреть как ведёт себя код в идентичных условиях:

list comprehension:
a) 0.0537 мс
b) 5.3700 мс
c) 5.7781 мс

generator expression:
a) 0.1512 мс
b) 13.0335 мс
c) 13.3203 мс

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

Заключение

Простая проблема, много решений, но только один победитель... ну или два. Я считаю разумным отдать первое место как for+extend, так и list comprehension. Первый вариант хорош тогда, когда ваша главная забота – производительность. Второй вариант – это действительно хороший выбор, когда вам нужно удобное решение в одну строчку.

Поэтому в следующий раз, когда вам нужно будет применить flat_map в Python, просто используйте list comprehension. И самое главное, не поддавайтесь соблазну использовать reduce или sum для длинных списков!

Оригинальный текст


Статья подготовлена для канала Hello World.