Python – очень обширный язык. Он предоставляет возможность писать код как в объектно-ориентированном стиле, так и в функциональном. Один из основных инструментов для функционального стиля, который даёт программирование на Python — map(). Это функция, которая применяет любую другую функцию ко всем элементам какой-либо последовательности (или нескольких). В этом уроке Вы узнаете, как и зачем её использовать.
- Функциональный стиль в Python
- map Python
- Первый аргумент: функция
- Примерmap()c встроенными функциями
- Примерmap()c пользовательскими функциями
- Примерmap()c анонимными функциями
- Примерmap()c методами
- Второй аргумент: итерируемый объект
- Обработка множественных итераций с помощью map()
- Возвращаемое значение: итератор
- MapReduce
- Пишем MapReduce
Функциональный стиль в Python
Функциональное программирование – это парадигма, которая рассматривает программу, как совокупность функций (в математическом смысле). Функция – это блок кода, который принимает входные данные, производит какую-то работу и возвращает результат. Если функция не имеет побочных эффектов – влияет только на данные, которые возвращает – её называют чистой функцией. Если большинство Ваших функций чистые – это хорошо, так как такие функции создают более стройную архитектуру, в которой проще разобраться, искать ошибки, вносить изменения и так далее.
Пример чистой функции:
def pure(x):
y = x + 500
return y
Пример функции с побочным эффектом:
def pure(x):
y = x + 500
print(y)
return y
Эта функция имеет побочный эффект, ведь она не только производит операции над аргументами и возвращает их результат, но и осуществляет вывод в консоль.
Считается, что функциональный стиль программирования позволяет писать более простой код. По своему опыту скажу, что это действительно так, пока дело не доходит до каких-то по-настоящему сложных задач.
Функциональное программирование включает в себя, как минимум, следующие инструменты:
— Возможность вызвать функцию для каждого элемента последовательности отдельно. На выходе получаем новую последовательность, состоящую из результатов поэлементного выполнения функции. В Питоне представлена функцией map().
— Возможность вызвать функцию-фильтр (реализующую определённое условие) для каждого элемента последовательности отдельно. На выходе получаем новую последовательность, состоящую только из элементов, удовлетворяющих условиям фильтра. В Питоне представлена функцией filter().
— Возможность вызвать функцию для каждого элемента последовательности отдельно, которая накапливает значение. На выходе получаем одно значение. В Питоне представлена функцией reduse().
— Замыкания – функции, которые запоминают своё состояние.
— Анонимные функции – функции без имени. Представлены ключевым словом Python lambda (лямбда).
Не смотря на то, что Пайтон ориентирован в первую очередь на объектно-ориентированный стиль, в нём есть и другие инструменты из функционального стиля программирования, к примеру, функция Python zip(), которая объединяет несколько последовательностей, enumerate(), которая возвращает пары индекс-элемент, списковые включения и так далее.
map Python
map в Python 3 – это функция, которая принимает другую функцию и одну или несколько итерируемых объектов, применяет полученную функцию к элементам полученных итерируемых объектов и возвращает специальный объект map, который является итератором и содержит результаты. Самый простой способ получить результаты из итераторы, это преобразовать его в коллекцию – использовать функции list(), set() или tuple()
Функция Python map() имеет следующий синтаксис:
map(function, iterable, [iterable_2, iterable_3, ...])
Можно сказать, что функция map() перебирает элементы коллекций в цикле и на каждой итерации применяет переданную функцию. Таким образом, тоже самое можно сделать при помощи обычного цикла или спискового значения. Следующие три фрагмента кода идентичны по результатам выполнения:
collection = range(10)
def my_func(x):
return x**4
print('map:', list(map(my_func, collection)))
print('Списковое включение:', [my_func(x) for x in collection])
result = []
for item in collection:
result.append(my_func(item))
print('for:', result)
# Вывод:
map: [0, 1, 16, 81, 256, 625, 1296, 2401, 4096, 6561]
Списковое включение: [0, 1, 16, 81, 256, 625, 1296, 2401, 4096, 6561]
for: [0, 1, 16, 81, 256, 625, 1296, 2401, 4096, 6561]
Что выгодно отличает map(), так это скорость выполнения. Дело в том, что этой функции не нужно создавать копии элементов для вычислений и, из-за этого она часто оказывается быстрее альтернативных вариантов. Кроме того, вы можете отложить вычисления, если не станете сразу преобразовывать итератор в последовательность.
Давайте увеличим вычислительную сложность и замерим скорость выполнения всех трёх способов:
from time import time
collection = range(10 ** 7)
def my_func(x):
return x ** 4 ** 4
now = time()
tuple(map(my_func, collection))
print('map:', time() - now)
now = time()
[my_func(x) for x in collection]
print('Списковое включение:', time() - now)
now = time()
result = []
for item in collection:
result.append(my_func(item))
print('for:', time() - now)
# Вывод:
map: 134.0279233455658
Списковое включение: 135.18106865882874
for: 97.16599297523499
Как видите, map() оказалась быстрее спискового включения.
Ещё одним преимуществом является то, что map() может принимать несколько коллекций. Другими методами это сделать сложнее.
Первый аргумент: функция
Первый параметр карты функции map – функция. При чём, этой функцией может быть как встроенная, так и пользовательская, а, кроме того, подходят анонимные функции. Даже метод можно передать в качестве первого аргумента.
- Встроенные — это заранее созданные функции
- Пользовательские – те, что пишет программист с помощью ключевого слова def
- Анонимные – функции, у которых нет имени, и они объявляются при помощи ключевого слова lambda
- Методы – функции, которые принадлежат определённым class objects
Пример map() c встроенными функциями
from math import sqrt
collection = range(10)
print(tuple(map(sqrt, collection)))
# Вывод:
(0.0, 1.0, 1.4142135623730951, 1.7320508075688772, 2.0, 2.23606797749979, 2.449489742783178, 2.6457513110645907, 2.8284271247461903, 3.0)
Здесь мы использовали встроенную функцию sqrt из модуля math стандартной библиотеки. Эта функция возвращает корень числа.
Пример map() c пользовательскими функциями
from random import randint
collection = [randint(100, 7000) for i in range(10)]
def my_func(var: int):
print(chr(var), end=' ')
set(map(my_func, collection))
# Вывод:
Ӈ ر ᣐ ᧒ Ɵ ؆ ణ ག ᘁ ᎋ
Здесь мы сперва заполняем список случайными целыми числами, лежащими в диапазоне от 100 до 7000. Затем определяем пользовательскую функцию при помощи ключевого слова def. Она принимает целое число в параметр var и печатает в терминале символ, соответствующий этому числу. Последняя команда – выполнить пользовательскую функцию my_func к каждому элементу последовательности collection с помощью map(). Поскольку функция map возвращает объект map, его нужно конвертировать в последовательность. В данном случае итератор преобразуется в множество при помощи встроенной функции set().
Обратите внимание, что в функции my_func нет слова return – используется исключительно побочный эффект print. В таком случае значение всё равно возвращается, но оно равно None. Проверить это можно распечатав итератор map():
from random import randint
collection = [randint(100, 7000) for i in range(10)]
def my_func(var: int):
print(chr(var), end=' ')
print('\n' + str(tuple(map(my_func, collection))))
# Вывод:
ᢢ ฌ Й ᄣ ͯ ݑ ᒀ მ ᄼ
(None, None, None, None, None, None, None, None, None, None)
Пример map() c анонимными функциями
Распространённый вариант использования функции map() – передача лямбда-функции.
Эти функции выглядят следующим образом:
lambda параметры: выражение
Лямбда-функции, как правило используются только один раз. С их помощью можно принять любое количество аргументов, а значение, которое будет возвращено, определяется выражением, указанным после двоеточия.
collection = range(10)
print(tuple(map(lambda x: x**x, collection)))
# Вывод:
(1, 1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489)
В этом примере lambda принимает один аргумент x и возвращает x в степени x. Как видите, такие функции прекрасно работают с map().
Пример map() c методами
Да, методы – это про объектно-ориентированное программирование. Но, в том и прелесть Python, что подходы можно комбинировать.
collection = range(10)
my_list = []
tuple(map(my_list.append, collection))
print(my_list)
# Вывод:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
В этом примере мы создали переменную my_list типа список. Далее, в map() мы вызываем метод этого объекта .append(), который добавляет элементы в конец списка.
Второй аргумент: итерируемый объект
Наиболее часто встречающийся итерируемый тип в Питоне – это список. Это связано с тем, что объекты данного типа действительно очень удобны. Но, есть и другие: множество, кортеж, словарь, строка и другие.
collection = range(10)
my_list = list(collection)
my_tuple = tuple(collection)
my_set = set(collection)
my_dict = dict(enumerate(collection))
my_str = [str(i) for i in collection]
result_list = list()
tuple(map(result_list.append, my_list))
tuple(map(result_list.append, my_tuple))
tuple(map(result_list.append, my_set))
tuple(map(result_list.append, my_dict))
tuple(map(result_list.append, my_str))
print(result_list)
# Вывод:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
Здесь Вы можете наблюдать как функция map() отработала со всеми перечисленными выше итерируемыми типами данных.
Обработка множественных итераций с помощью map()
В map() можно передать несколько итерируемых объектов. В этом случае элементы каждого из них будут переданы в функцию как позиционные аргументы в том же порядке, в каком Вы их перечислили в вызове map(). К сожалению, именованные аргументы передавать таким способом нельзя. Если итерируемые объекты имеют разную длину, map() сделает столько итераций, сколько в самой короткой коллекции.
collection = range(10)
def my_func(x: int, y: int):
return x ** y
result_tuple = tuple(map(my_func, collection, collection))
print(result_tuple)
# Вывод:
(1, 1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489)
В этом примере мы передали одну и ту же последовательность collection дважды, а также пользовательскую функцию, которая принимает два аргумента и возвращает первый из них, возведённый в степень, равную второму. Можно убедиться что результирующая коллекция содержит столько же элементов, сколько и в переданных списках.
Теперь пример с последовательностями разной длины:
collection = range(10)
def my_func(x: int, y: int):
return (x, y)
result_tuple = tuple(map(my_func, collection, collection[:4:-1]))
print(result_tuple)
# Вывод:
((0, 9), (1, 8), (2, 7), (3, 6), (4, 5))
Здесь мы передаём вторую коллекцию вдвое короче первой. Как видите, в итоговом кортеже всего 5 элементов.
Возвращаемое значение: итератор
Функция Python map() возвращает специальный объект map, который является итератором. Как уже говорилось, его можно преобразовать в список, множество или кортеж с помощью встроенных функций:
collection = range(10)
def my_func(x: int, y: int):
return (x, y)
result = tuple(map(my_func, collection, collection[:4:-1]))
print('tuple:', result)
result = list(map(my_func, collection, collection[:4:-1]))
print('list:', result)
result = set(map(my_func, collection, collection[:4:-1]))
print('set:', result)
result = dict(enumerate(map(my_func, collection, collection[:4:-1])))
print('dict:', result)
# Вывод:
tuple: ((0, 9), (1, 8), (2, 7), (3, 6), (4, 5))
list: [(0, 9), (1, 8), (2, 7), (3, 6), (4, 5)]
set: {(2, 7), (1, 8), (0, 9), (4, 5), (3, 6)}
dict: {0: (0, 9), 1: (1, 8), 2: (2, 7), 3: (3, 6), 4: (4, 5)}
А можно отложить вычисления до менее загруженного вычислениями места кода или вычислять итерации по одной в тот момент, когда это нужно:
collection = range(10)
def my_func(x: int, y: int):
return (x, y)
map_object = map(my_func, collection, collection[:4:-1])
print(map_object.__next__())
print(map_object.__next__())
print(map_object.__next__())
print(list(map_object))
# Вывод:
(0, 9)
(1, 8)
(2, 7)
[(3, 6), (4, 5)]
В этом примере мы выполнили три раза по одной итерации при помощи дандер-метода .__next__(), а затем вычислили все оставшиеся итерации при помощи функции list().
MapReduce
MapReduce – это модель распределённых вычислений, разработанная в корпорации Google, применяемая в технологиях Big Data для распределённых вычислений над гигантскими массивами данных в узлах компьютерных кластеров.
MapReduce можно с уверенностью назвать главной технологией Big Data. Суть MapReduce состоит в разделении информационного массива на части, распределённой обработки каждой части на отдельном узле и финального объединения всех результатов.
Сегодня множество различных, как коммерческих, так и свободных продуктов, использующих эту модель распределенных вычислений: Apache Hadoop, Apache CouchDB, MongoDB, в графических процессорах NVIDIA с использованием CUDA, MySpace Qizmt и прочие.
Авторами данной модели считаются сотрудники компании Google Джеффри и Санджай Гемават, взявшие за основу две процедуры функционального программирования: map, применяющая нужную функцию к каждому элементу последовательности, и reduce, объединяющая результаты работы map.
Другими словами, упрощённо эта модель состоит в том, что массив данных делится на части, затем выполняются одинаковые вычисления на разных серверах с разными частями массива, а затем результаты вычислений вновь объединяются.
Предлагаю провести эксперимент и воссоздать MapReduce на Вашем собственном компьютере.
Пишем MapReduce
Вместо нескольких серверов будем использовать процессы. Процессы – это как независимые программы, которые могут вычисляться на разных ядрах процессора. Для этого нам понадобится модуль multiprocessing из стандартной библиотеки. В этом модуле есть объект Pool, представляющий из себя несколько процессов. У объекта Pool есть метод .map(), который идентичен уже изученной ранее функции map().
from multiprocessing import Pool
from time import time
def l(x):
return x ** 1000
y = list(range(100000))
if __name__ == '__main__':
a = Pool()
now = time()
sum(list(a.map(l, y)))
print('Вычисления в пуле процессов заняли:', time() - now)
now = time()
sum(list(map(l, y)))
print('Вычисления в одном процессе заняли:', time() - now)
# Вывод:
Вычисления в пуле процессов заняли: 1.8741416931152344
Вычисления в одном процессе заняли: 4.924708127975464
Здесь роль reduce, то есть обобщения результатов, взяла на себя функция sum(), которая вычисляет сумму всех элементов последовательности.