Почему в Python по-прежнему нужна функция map()

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

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

Большинство статей о map() в Python просто показывают, как использовать эту функцию, но не объясняют, почему ее нужно использовать. Неудивительно, что, несмотря на популярность map() среди авторов Python, она кажется недооцененной разработчиками среднего уровня.

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

Немного о функции map()

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

В качестве аргумента принимается callable, поэтому речь идет о функции высшего порядка. Поскольку это типичная особенность функциональных языков программирования, map() соответствует стилю функционального программирования.

Но по моему мнению, map() скорее использует похожий API, чем является элементом истинно функционального программирования. Дело в том, что map() может использоваться для “нечистых” функций, то есть функций, имеющих побочные эффекты, а это недопустимо в настоящем функциональном программировании.

Пришло время увидеть map() в действии.

>>> numbers = [1, 4, 5, 10, 17]
>>> def double(x):
... return x*2

Итак, у нас есть список чисел и функция, которая удваивает число. double() работает для одного числа:

>>> double(10)
20

Что произойдет, если использовать double() для numbers?

>>> double(numbers)

[1, 4, 5, 10, 17, 1, 4, 5, 10, 17]

Для того, кто знает, как работает умножение списка в Python, здесь нет ничего удивительного. Это нормальное поведение, но совсем не то, чего мы хотели добиться.

Приведенная выше строка double(numbers) применила функцию double() к numbers в целом (как к объекту). Нам же нужно применить double() к каждому элементу numbers. Такой принцип имеет в этом случае большое значение, и именно здесь вступает в игру map(): ее можно использовать, когда необходимо применить callable к каждому элементу итерируемого объекта.

Предупреждение: некоторые языки используют имя map для хеш-таблиц, а в Python словари являются хеш-таблицами. Поэтому, когда вы видите термин “map” в другом языке, сначала посмотрите, что он обозначает. Например, map() в R эквивалентна map() в Python, но в Go map() создает хеш-таблицу и работает аналогично dict() в Python.

Вот как следует использовать map():

>>> doubled_numbers = map(double, numbers)

Как видите, вы передаете map() в качестве первого аргумента callable, а в качестве второго  —  итерируемый объект. Это возвращает объект map (если речь идет о Python 3, но в Python 2 вы получили бы список):

>>> doubled_numbers #doctest: +ELLIPSIS
<map object at ...>

Обратите внимание, что я использовал #doctest: +ELLIPSIS, так как этот документ предусматривает doctest (доктесты). С их помощью я убедился, что все примеры корректны. Подробнее о doctest можете прочитать в документации.

Объект map работает как генератор. Поэтому, даже используя список в map(), мы получим не список, а генератор. Генераторы оцениваются по требованию (лениво). Если вы хотите преобразовать объект map в список, используйте функцию list(), которая оценит все элементы:

>>> list(doubled_numbers)
[2, 8, 10, 20, 34]

Вы также можете оценить элементы map любым другим способом, например в цикле for. Чтобы избежать проблем, помните: после оценки такого объекта он становится пустым и больше не может быть оценен.

>>> list(doubled_numbers)
[]

Выше функция map() была применена для одного итерируемого объекта, но вы можете использовать любое их количество. Функция будет работать на основе их индекса, то есть сначала она вызовет callable для первых элементов итерируемых объектов (с индексом 0), затем для следующих и так далее.

Простой пример:

>>> def sum_of_squares(x, y, z):
...     return x**2 + y**2 + z**2

>>> x = range(5)
>>> y = [1, 1, 1, 2, 2]
>>> z = (10, 10, 5, 5, 5)
>>> SoS = map(sum_of_squares, x, y, z)
>>> list(SoS)
[101, 102, 30, 38, 45]
>>> list(map(sum_of_squares, x, x, x))
[0, 3, 12, 27, 48]

Альтернативы map()

Вместо map() можно использовать генератор, например посредством выражения-генератора:

>>> doubled_numbers_gen = (double(x) for x in numbers)

Получим генератор, как и в случае с map(). Если нужен список, лучше воспользоваться соответствующим генератором списков: 

>>> doubled_numbers_list = [double(x) for x in numbers]

Что более читабельно: версия map() или выражение-генератор (или генератор списков)? Для меня, несомненно, более понятны выражение-генератор и генератор списков, хотя я без проблем понимаю версию map(). Но я знаю, что некоторые специалисты предпочтут версию map(), особенно те, кто недавно перешел на Python с другого языка, в котором используется функция, похожая на map().

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

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

# версия map-lambda
map(lambda x: x*2, numbers)

# версия с генератором
(x*2 for x in numbers)

Эти две строки приводят к одинаковым результатам, единственное различие заключается в типе возвращаемых объектов: первая возвращает объект map, а вторая  —  объект generator.

Вернемся на минутку к многоитерируемому использованию map():

>>> SoS = map(sum_of_squares, x, y, z)

Можно переписать это с помощью выражения-генератора следующим образом:

>>> SoS_gen = (
...     sum_of_squares(x_i, y_i, z_i)
...     for x_i, y_i, z_i in zip(x, y, z)
... )

>>> list(SoS_gen)
[101, 102, 30, 38, 45]

На этот раз я предпочту версию map()! Помимо того, что она более лаконична, она, на мой взгляд, гораздо понятнее. В версии с генератором применяется функция zip(). Даже если эта функция проста в использовании, она увеличивает сложность команды.

Так нужна ли функция map()?

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

В таком случае нужна ли вообще функция map()?

Размышляя над этим вопросом, я пришел к трем основным причинам, почему в Python нужна map().

1. Производительность

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

Однако помните, что это не общее правило. Поэтому проверяйте, будет ли map() быстрее работать в вашем фрагменте кода.

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

2. Параллелизм и потоки

При распараллеливании кода или использовании пула потоков обычно используются функции, похожие на map(). Это могут быть такие методы, как multiprocessing.Pool.map(), pathos.multiprocessing.ProcessingPool.map() и concurrent.futures.ThreadPoolExecutor.map().

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

import multiprocessing
def double(x):
return x*2

if __name__ == "__main__":
numbers = [1, 4, 5, 10, 17]
mode = "parallel" # or "sequential"
if mode == "parallel":
p = multiprocessing.Pool(2)
map_func = p.map
elif mode == "sequential":
map_func = map
else:
raise NotImplementedError("Use mode of 'parallel' or 'sequential'")
results = list(map_func(double, numbers))
if mode == "parallel":
p.close()
print(results)

Конечно, в этом простом примере распараллеливание не имеет смысла и будет медленнее. Это просто пример.

3. Простота для новичков, перешедших на Python с других языков

Эта причина нетипична и не имеет отношения к самому языку, но иногда она важна. Для меня выражение-генератор почти всегда проще в написании и более читабельно. Однако, когда я был совсем новичком в Python, генераторы давались мне с трудом как в написании, так и в понимании. Но поскольку я пришел в Python после 16 лет программирования на R, я был хорошо знаком с функцией map() в R, которая работает точно так же, как и map() в Python. Поэтому мне было гораздо проще использовать map(), чем соответствующее выражение-генератор или генератор списка. 

Более того, знакомство с map() помогло мне понять эти генераторы. Я также смог овладеть стилем Python. Мы знаем, что третий вариант, который здесь не рассматривался,  —  это цикл for, но он редко бывает оптимальным (или даже просто хорошим) методом.

Таким образом, если кто-то приходит в Python и знает, как работают подобные функции, ему гораздо проще писать подобный код. А вот, к примеру, человек, пришедший в Python из языка C, скорее всего, будет использовать цикл for.

Следовательно, map() является мостом между Python и другими языками.

4. Читабельность при нескольких итерируемых объектах

Как было показано выше, когда нужно использовать callable для нескольких итерируемых объектов одновременно, map() способствует читабельности и лаконичности в большей мере, чем соответствующее выражение-генератор. Таким образом, даже если в простых ситуациях map() менее читаема, в более сложных сценариях читабельность становится сильной стороной этой функции.


Заключение

Некоторые представители сообщества говорят, что в Python не нужна функция map(). Еще в 2005 году создатель Python Гвидо ван Россум хотел удалить ее из Python вместе с filter() и reduce(). Но сегодня, 17 лет спустя, мы все еще можем использовать ее. Искренне надеюсь, что мы будем применять ее и в дальнейшем. Это подводит нас к двум важнейшим вопросам, поднятым в этой статье.

  • Является ли функция map() обязательной в Python? Нет, не является. Вы можете достичь того же результата с помощью альтернативных методов.
  • Нужна ли вообще функция map() в Python? Да, нужна. Не потому, что она обязательна, а потому, что все еще используется и служит различным полезным целям.

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

По этим причинам функция map(), на мой взгляд, заслуживает своего места в кодовой базе Python, даже если используется не слишком часто.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Marcin Kozak: Does Python still need the map() function?

Предыдущая статья5 вечерних практик, которые помогают избежать выгорания
Следующая статьяКак использовать React в приложениях Angular