Плюсы и минусы использования функций Map и Filter в Python Python

Плюсы и минусы использования функций Map и Filter в Python

Кратко о функциях Map и Filter

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

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

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

Преобразование элементов с помощью функции map

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

def kvadrat(n):
  return n ** 2

chisla = [2, 1, 3, 4, 7, 11, 18]
kvadratd_chisla = map(kvadrat, chisla)

В результате, функция map() вернет нам ленивый итерируемый объект:

print(kvadratd_chisla)
# <map object at 0x000001F7B0AFBD90>

Давайте теперь более подробно рассмотрим, как далее должен будет работать объект kvadratd_chisla, созданный в нашем примере функцией map():

  • Итак, для того, чтобы подключить данный map-объект к работе, прежде всего, следует обеспечить перебор его элементов либо в любом цикле, либо с помощью итерационных функций типа конструктора списка list().
  • Внутри этих циклов (итерационных функций) каждый элемент вышеназванного map-объекта будет работать с такими аргументами, сгенерировавшей его функции map(), как пользовательская функция kvadrat() и исходный итерируемый объект – список chisla.
  • В результате для каждого элемента map-объекта kvadratd_chisla через пользовательскую функцию kvadrat() будет выполнятся определенный расчет на основе использования соответствующего элемента из исходного списка chisla.

Теперь давайте выведем на консоль то, что у нас получилось при итерации элементов map-объекта kvadratd_chisla через функцию конструктора списка list():

print(list(kvadratd_chisla))
# [4, 1, 9, 16, 49, 121, 324]

Как мы видим, map-объект возвел в квадрат каждое число из списка chisla.

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

Итак, давайте кратко перечислим основные особенности работы с функцией map():

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

Фильтрация элементов последовательностей с помощью функции Filter

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

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

Давайте попробуем разобраться в работе вышеназванной функции с помощью соответствующего примера, в котором будем использовать все тот же список chisla и пользовательскую функцию для проверки четности nechetnost():

def nechetnost(n):
  return n % 2 == 1

chisla = [2, 1, 3, 4, 7, 11, 18]
nechetnyye_chisla = filter(nechetnost, chisla)

Подобно map(), функция filter() возвращает нам ленивый итерируемый объект:

print(nechetnyye_chisla)
# <filter object at 0x0000011CE197BD90>

Теперь же давайте рассмотрим, как далее будет работать сгенерированный функцией filter() объект nechetnyye_chisla, выполняющий фильтрацию исходного списка по нечетным числами:

  • Для того, чтобы наш filter-объект перебирал соответствующие элементы итерируемого объекта (списка chisla), его точно также, как и map-объект, следует либо поместить в любой цикл, либо применить к нему итерационные функции типа конструктора списка list().
  • Внутри вышеназванных циклов или итерационных функций наш filter-объект для каждого элемента списка chisla вызывает предварительно заданную нами пользовательскую функцию nechetnost().
  • В свою очередь, данная пользовательская функция осуществляет проверку чисел исходного списка chisla и включает в результирующий итог filter-объекта nechetnyye_chisla только нечетные числа.
print(list(nechetnyye_chisla))
# [1, 3, 7, 11]

Итак, мы видим, что в результирующем объекте, сгенерированном filter() присутствуют только нечетные числа, которые были включены в filter-объект нашей пользовательской функцией nechetnost() поскольку отвечали параметру True (истинное значение) при передачи через эту функцию элементов нашего исходного списка chisla.

Примечание: Важно отметить, что функция filter() не возвращает все элементы итерируемого объекта, как это было с функцией map(), а возвращает лишь те его элементы, которые удовлетворяют условию, заданному в первом аргументе filter(), ассоциирующему в нашем случае с пользовательской функцией nechetnost()*.

Итак, давайте подытожим то, что мы уже узнали о работе функций *filter():

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

Схожесть функций Map и Filter с генераторами-выражений

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

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

Однако, те же самые действия можно выполнить и с помощью специальной конструкции на основе цикла – генератора-выражения. Давайте посмотрим, как это выглядит на практике:

def kvadrat(n):
  return n ** 2

def map_analog(function, iterable):
  return (function(x) for x in iterable)

chisla = [2, 1, 3, 4, 7, 11, 18]
kvadratd_chisla = map_analog(kvadrat, chisla)

print(list(kvadratd_chisla))
# [4, 1, 9, 16, 49, 121, 324]

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

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

Давайте посмотрим, как выглядит код такого генератора – выражения на практике:

def nechetnost(n):
  return n % 2 == 1

def filter_analog(function, iterable):
  return (x for x in iterable if function(x))

chisla = [2, 1, 3, 4, 7, 11, 18]
nechetnyye_chisla = filter_analog(nechetnost, chisla)

print(list(nechetnyye_chisla))
# [1, 3, 7, 11]

В итоге, мы получим тот же результат, что и при использовании обычной функции filter(), поскольку генератор-выражения перебирая итерируемый объект, будет вызывать функцию nechetnost() для каждого элемента chisla в той части выражения-генератора, где указано условие. Таким образом, наш генератор-выражения будет определять элементы, которые будут включены в новый «ленивый» итерируемый объект.

Чем функции filter и map уступают генераторам

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

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

С помощью функций map() и filter(), эта задача решается с учетом того, что мы предварительно задаем нашей программе соответствующий исходный список chisla:

chisla = [2, 1, 3, 4, 7, 11, 18]

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

Для этого мы сначала используем пользовательскую функцию nechetnost() и список chisla в качестве аргументов функции filter(), а затем полученный «ленивый» итерационный объект с нечетными числами, передаем функции map() для обработки второй пользовательской функцией kvadrat()*.

В этом случае, наш код будет иметь следующий вид:

chisla = [2, 1, 3, 4, 7, 11, 18]
nechetnyy_kvadrat = map(kvadrat, filter(nechetnost, chisla))
print(nechetnyy_kvadrat)
# <map object at 0x0000020A4582B1F0>

В результате получился опять же «ленивый» итерируемый объект, содержащий квадраты нечетных чисел из нашего исходного списка chisla. Теперь, для уверенности, давайте убедимся, что полученный нами map-объект nechetnyy_kvadrat содержит именно то, что мы и предполагаем:

nechetnyy_kvadrat = map(kvadrat, filter(nechetnost, chisla))
print(list(nechetnyy_kvadrat))
# [1, 9, 49, 121]

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

nechetnyy_kvadrat = (kvadrat(n) for n in chisla if nechetnost(n))
print(nechetnyy_kvadrat)
# <generator object <genexpr> at 0x000001DE9BFF1EE0>

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

nechetnyy_kvadrat = [kvadrat(n) for n in chisla if nechetnost(n)]
print(nechetnyy_kvadrat)
# [1, 9, 49, 121]

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

print(list(map(kvadrat, filter(nechetnost, chisla))))
# [1, 9, 49, 121]

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

А теперь, сравните, приведенный код с версией кода, сформированного на основе генератора-выражения:

print([kvadrat(n) for n in chisla if nechetnost(n)])
# [1, 9, 49, 121]

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

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

Мы можем еще более упростить наш код, оформив его в виде простого цикла и поместив нужные нам операции, ранее записанные в виде пользовательских функций, внутрь квадратных скобок генератора в первую и последнюю его части:

print([n**2 for n in chisla if n % 2 == 1])
# [1, 9, 49, 121]

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

Выводы

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

  • Функция map() выполняет преобразование каждого элемента в итерируемом объекте, помещая эти элементы после преобразования в новый, «ленивый» итерируемый объект.
  • Функция filter() проводит фильтрацию элементов итерируемого объекта, возвращая новый (отфильтрованный), «ленивый» итерируемый объект.
  • В отдельных случаях, рекомендуется заменять использование map() и filter() функций на генераторы-выражений или генераторы списков, что позволяет сделать наш код более читабельным и простым для понимания.
Практический Python для начинающих
Практический Python для начинающих

Станьте junior Python программистом за 7 месяцев

 7 месяцев

Возможно будет интересно

🏆 Hello, world! Python
Новичок
🏆 Hello, world!

Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.

2022-10-04
Как практиковаться в Python? Python
Новичок
Как практиковаться в Python?

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

2022-10-19
Условные конструкции и сопоставление структурных шаблонов Шпаргалки
Новичок
Условные конструкции и сопоставление структурных шаблонов

Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов

2022-11-09