Сегодня вспомним принципы функционального программирования и разберём один из полезных инструментов этого подхода в языке Python — как работает функция map()
и что с ней можно делать.
Функциональное программирование
Перед тем как переходить к map()
, вспомним про подходы к программированию.
Программирование по принципам и идеям делится на несколько основных подходов-парадигм. Есть две основные — императивная и декларативная, и каждая из них включает много других.
Функциональное программирование не означает фокус на функциях. В центре этого подхода лежат более широкие идеи:
- Данные не изменяются после создания.
- Программы строятся из функций как из блоков. Мы описываем не чёткие команды, а правила взаимодействия этих блоков.
- Вычисления проводятся только в тот момент, когда действительно нужен результат. Это называется «ленивые вычисления».
- То, что происходит в функциях, не изменяет состояние программы и не зависит от условий. Это означает, что одни и те же данные на входе всегда дадут одни и те же значения на выходе.
- Функции могут принимать другие аргументы в качестве функций.
Некоторые языки программирования построены так, что писать код по-другому и не применять эти правила невозможно. В других идеи функционального программирования приходится встраивать в свой код.
Главные мысли во всём этом — программа должна производить чистые вычисления, которые не затрагивают её саму и внешние системы. А ещё — логика строится на основе композиции из функций, которые обеспечивают такие чистые вычисления.
Если бы мы говорили только про функции, то основной идеей было бы просто использование функций. Но в парадигме полагается использовать не любые функции, а те, которые подходят под всю концепцию.
Какие функции должны быть в функциональном программировании
Функциональное программирование основано на использовании чистых функций, которые:
- всегда возвращают одинаковый результат для одного и того же набора входных данных;
- не изменяют состояние программы и не взаимодействуют с внешними системами — файлы, базы данных, устройства ввода/вывода;
- можно заменить на результат выполнения, и это ничего не изменит — например, функцию возведения в квадрат можно заменить на квадрат нужного числа.
Одна из таких чистых функций — map()
в Python.
Введение в функцию map()
В Python map()
— функция, которая берёт другую функцию и применяет её к элементам итерируемого объекта. Итерируемые объекты — составные коллекции элементов, которые можно перебирать. В Python это списки, словари и кортежи.
Map()
— функция высшего порядка. Это значит, что она принимает другие функции в качестве аргумента или возвращает их или всё сразу.
👉 Важно то, что map()
в Питоне не изменяет объекты, с которыми работает, а создаёт новые. Это отвечает идее чистого функционального программирования, когда вычисления не влияют на остальной код.
Обычно для работы с итерируемыми объектами в Python используют циклы for
, но у map()
есть преимущества:
- Скорость. Хотя map используется в Python, она написана на C и оптимизирована, поэтому может работать быстрее цикла.
- Экономия ресурсов. При использовании цикла в памяти хранится весь список, а map использует ленивые вычисления: всё подсчитывается по запросу, и в памяти хранится только один элемент за раз.
Map(), filter() и reduce()
Изначально функцию map()
Python не включал, но в 1993 году добавили её и ещё две других: map()
, filter()
и reduce()
.
Эти три функции позволяют реализовать три основные техники при написании функционального кода:
- Mapping (отображение). Применение функции к каждому элементу итерируемого объекта. В результате mapping появляется новый объект, а старый остаётся неизменным. Например, создание списка квадратов исходных чисел.
- Filtering (фильтрация). Исключение элементов, которые не соответствуют предикату — условию. Например, мы хотим получить список квадратов из чисел, которые хранятся в исходном списке. Но нам нужны не все числа, а только чётные. Условие проверит первоначальный список, вернёт False для всех нечётных чисел, и при создании нового объекта они будут исключены.
- Reducing (редукция). Это объединение всех элементов в одно значение. Например, если нужно найти сумму, произведение или максимальное число.
Сначала — про mapping и про map()
.
Синтаксис функции map()
Разберём, как работает map
.
Основной синтаксис выглядит как ключевое слово map()
с двумя значениями в скобках: функцией и итерируемым объектом.
Вот пример: у нас есть список элементов из первых 10 букв кириллического алфавита:
rus_letters = ['а', 'б', 'в', 'г', 'д', 'е', 'ё', 'ж', 'з', 'и']
Мы хотим сделать новый список, добавив к каждой букве цифровой индекс. Например, 'а1', 'б1', 'в1'. Для этого мы создаём новую переменную result
, потому что при функциональном программировании нельзя изменять уже существующие данные.
После объявления переменной мы говорим компьютеру:
- создай список
list()
; - этот список нужно создать с помощью функции
map()
; - функцией для
map()
будет безымянная лямбда-функция, которая прибавляет '1' к каждому элементуx
; - элементы
x
берутся из списка с 10 буквами.
Так выглядит команда целиком:
use = list(map(lambda x: x + '1', ['а', 'б', 'в', 'г', 'д', 'е', 'ё', 'ж', 'з', 'и']))
Теперь, если вывести use
на экран командой print
, мы увидим новый список:
['а1', 'б1', 'в1', 'г1', 'д1', 'е1', 'ё1', 'ж1', 'з1', 'и1']
Использование функции map() с различными видами функций
В качестве функции map()
может принимать любую Python-функцию, которая принимает такое количество аргументов, сколько их содержится в итерируемом объекте.
Вот примеры того, как это может выглядеть.
Использование map() с лямбда-функциями. У обычных классических функций в Python есть имя, тело и результат. Лямбда-функции выглядят как одна строка. Вместо имени у них есть только ключевое слово lambda
.
Преобразование списка чисел в квадраты этих чисел этим путём будет выглядеть так:
# создаём список
numbers = [1, 2, 3, 4, 5]
# создаём новый список с использованием map
squares = list(map(lambda x: x**2, numbers))
# выводим результат на экран
print(squares)
В консоли вывода получим:
[1, 4, 9, 16, 25]
Использование map() со встроенными функциями. Иногда можно не придумывать свой алгоритм, а взять готовый. Например, вместо лямбда-функций использовать встроенные функции Python: str
, len
, abs
.
Пример — делаем из списка с целыми числами список из строк. Для этого применяем функцию str
на каждом элементе и создаём из числа строку:
# создаём список
numbers = [1, 2, 3, 4, 5]
# создаём новый список, используя map
string_numbers = list(map(str, numbers))
# выводим результат на экран
print(string_numbers)
Результат:
['1', '2', '3', '4', '5']
Использование map() с несколькими итерациями. Map()
умеет принимать сразу несколько итерируемых объектов. Тогда функция-аргумент должна принимать столько же аргументов, сколько объектов передано.
Можно передать два списка и сложить элементы под одними и теми же номерами, или индексами:
# создаём первый список
list1 = [1, 2, 3]
# создаём второй список
list2 = [4, 5, 6]
# создаём список из суммы элементов
# двух списков с одинаковыми индексами
sums = list(map(lambda x, y: x + y, list1, list2))
# выводим результат на экран
print(sums)
Запускаем код:
[5, 7, 9]
Примеры использования функции map()
Разберём работу map()
ещё на нескольких примерах.
Преобразование строковых итераций. Map()
можно использовать для преобразования списка строк, например изменения регистра.
Для этого применим встроенную функцию str()
к списку строковых объектов:
# создаём список строк
words = ['hello', 'world', 'python']
# создаём новый список и приводим
# все слова к верхнему регистру
uppercase_words = list(map(str.upper, words))
# выводим результат
print(uppercase_words)
В консоли получим:
['HELLO', 'WORLD', 'PYTHON']
Преобразование числовых итераций. Списки и другие изменяемые коллекции можно использовать, чтобы на их основе создать новые.
Пример. Создаём список из чисел и применяем к каждому элементу математическую операцию: увеличиваем все числа на 10.
# создаём список из целых чисел
numbers = [1, 2, 3, 4, 5]
# создаём новый список с помощью map,
# увеличивая каждое число исходного списка на 10
increased_numbers = list(map(lambda x: x + 10, numbers))
# выводим результат на экран
print(increased_numbers)
В новом списке все элементы — сумма изначальных элементов и 10:
[11, 12, 13, 14, 15]
Удаление пробелов и знаков препинания. Строки можно не только преобразовывать, но и очищать. Например, удалять ненужные пробелы и знаки препинания.
Удаляем пробелы из строк, применяя к элементам встроенный метод str.strip
:
# создаём список из объектов-строк с пробелами
phrases = [' hello ', ' world ', ' python ']
# создаём новый список и с помощью map проходим по каждому
# элементу методом strip(), который удаляет пробелы
cleaned_phrases = list(map(str.strip, phrases))
# выводим результат на экран
print(cleaned_phrases)
Получаем очищенную строку:
['hello', 'world', 'python']
Другой пример: импортируем модуль string
и получаем доступ к методу punctuation
, который знает все стандартные знаки препинания. Если этот метод передать в метод strip
, который очищает строки, можно очистить коллекции строк:
# импортируем модуль для работы со сроками
import string
# создаём список из объектов-строк со знаками препинания
phrases = ['hello!', 'world,', 'python.']
# создаём новый список и с помощью map и функции lambda удаляем все знаки препинания
object = list(map(lambda x: x.strip(string.punctuation), phrases))
# выводим результат на экран
print(object)
Проверяем результат:
['hello', 'world', 'python']
Конвертация температур. Map можно настроить на более интересные задачи. Например, задать формулу, по которой переводятся градусы из шкалы по Цельсию в шкалу Фаренгейта.
Для этого формулу нужно записать в лямбда-функцию:
# создаём список из целых чисел
celsius_temps = [0, 20, 30, 40]
# создаём новый список и с помощью lambda-функции
# применяем к каждому элементу формулу
# преобразования градусов по Цельсию в Фаренгейты
fahrenheit_temps = list(map(lambda c: c * 9/5 + 32, celsius_temps))
# выводим результат на экран
print(fahrenheit_temps)
Смотрим, что получилось:
[32.0, 68.0, 86.0, 104.0]
Недостатки функции map()
Map() — мощный инструмент, который можно настроить для разных ситуаций. Но минусы у этой функции тоже есть:
- В сочетании с lambda-функциями
map()
становится довольно сложной для понимания. Чем длиннее lambda-функция, тем сложнее разобраться в итоговом выражении. Map()
неудобно использовать при итерации вложенных объектов. Например, при работе со списками внутри списков проще будет воспользоваться другими инструментами языка.- В
map()
нет встроенного механизма для обработки ошибок в функции. Если внутри lambda-функции что-то сломается, разобраться может быть сложно.
Альтернативы функции map()
В Python есть и другие технологии со схожими возможностями: списковые включения и генераторные выражения.
Использование списковых включений, или генераторов списков (list comprehensions), — один из наиболее популярных способов преобразования данных в Python. Они понятные для чтения и гибкие.
Вот два примера, каждый из которых создаёт новый список, увеличивая исходный на 10:
# исходный список
item = [1, 2, 3]
# создаём новый список с функцией map()
result = list(map(lambda x: x + 10, item))
# создаём новый список со списочным включением
result = [x + 10 for x in numbers]
Видно, что генератор списков выглядит более читаемым: в нём меньше операторов, действий и аргументов.
Ещё код list comprehensions позволяет включать условия фильтрации, а в map()
такой возможности нет. Например, мы можем создать новый список из исходного, используя только чётные элементы:
result = [x + 10 for x in numbers if x % 2 == 0]
Использование генераторных выражений. Генераторные выражения похожи на списочные включения, но возвращают итератор вместо списка.
Что нужно вспомнить про итераторы и итерирование:
- Итератор (iterator) — это объект, который позволяет проходить по элементам коллекции и возвращать по одному элементу.
- Итерируемый объект (iterable) — объект, который возвращает эти элементы. Из него можно получить итератор.
- Итерировать — повторять какую-то операцию несколько раз. Например, перебирать буквы в слове.
Это экономит память, потому что избавляет от необходимости хранить всю последовательность в памяти целиком.
Когда мы используем циклы и перебираем все элементы списка или другой коллекции, внутри этой системы используется итератор.
Как это работает:
- Мы создаём список или другую коллекцию данных.
- Используем генераторное выражение на исходной коллекции и получаем итератор, внутри которого — новые значения.
- Итератор можно использовать разными способами — преобразовать в коллекцию или передать в другие функции, которые умеют работать с итераторами.
# создаём список
numbers = [1, 2, 3]
# используем генераторное выражение,
# которое возвращает итератор
iterables = (x + 10 for x in numbers)
Внутри получившегося итератора находятся целые числа: 11, 12 и 13. По ним можно пройтись циклом или переделать в итерируемый объект-коллекцию, но сейчас это именно итератор, а не итерируемый объект.
Сравнение map() с другими функциями
Map()
— одна из трёх функций, которые часто составляют основу функциональной программы. Если сравнить их, то получатся примерно такие ключевые наблюдения:
- map() преобразует элементы;
- filter() отбирает элементы;
- reduce() сводит коллекцию к одному значению, комбинируя элементы.
Сравнение с filter(). Map()
берёт готовые элементы и на их основе делает новый итерируемый объект. А filter()
используется для отбора элементов, которые удовлетворяют определённому условию (предикату).
Синтаксис map()
и filter()
похож. Так будет работать отбор чётных чисел из существующего списка:
# создаём новый список
numbers = [1, 2, 3, 4, 5]
# создаём итератор на основе списка
result = list(filter(lambda x: x % 2 == 0, numbers))
Сравнение с reduce(). Эта функция — часть модуля functools. Она последовательно применяет функцию, чтобы свернуть итерируемый объект до одного значения.
Объясним на примере:
# импортируем reduce из модуля functools
from functools import reduce
# создаём список
numbers = [1, 2, 3, 4]
# суммируем элементы списка
result = reduce(lambda x, y: x + y, numbers)
# выводим результат на экран
print(result)
Результатом работы reduce()
будет сумма всех элементов списка — 10. Для этой цели можно было использовать другую встроенную функцию — sum()
. Она тоже отвечает требованиям функционального программирования, но не умеет делать остальных вещей, которые можно делать с reduce, например умножения чисел или объединения строк.