MyTetra Share
Делитесь знаниями!
Лямбда-функции в языке Python. Использование map/filter/reduce. Простое объяснение
Время создания: 19.04.2023 08:52
Автор: Xintrea
Текстовые метки: python, map, filter, reduce, лямбла, lambda, функция, просто, функциональное, программирование
Раздел: Компьютер - Программирование - Язык Python
Запись: xintrea/mytetra_syncro/master/base/16818835543q2t2gx7zs/text.html на raw.github.com

Триада функций map/filter/reduce пришла в язык Python из функционального программирования. Самый известный функциональный язык программирования называется Lisp, что расшифровывается как List Processing language — т. е. «язык обработки списков». А это значит, что данные механизмы предназначены для массовой обработки списков. И если быть еще точнее — для обработки элементов списка.


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



Что такое лямбда-функция?


Лямбда-функция - это такая же функция как и любая другая, только безымянная. А так как у нее нет имени, то к данной функции можно обратиться только в том месте, где она и написана. Вот и все.


Как следствие, лямда-функция - это "одноразовая" функция. Из-за того что она безымянная, ее можно использовать, буквально, только в том месте, где она написана. То есть, невозможно вызвать лямбда-функцию из нескольких мест в коде. Обычно, классические функции часто пишут для того, чтобы избежать дублирования кода либо для лучшего структурирования программы. В любом случае, у классических функций есть место описания функции и, как минимум, одно место вызова функции. Лямбда-функция, в свою очередь, используется только в одном месте, а именно там, где она написана, и все.


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


Пример обычной функции:



def standard_function(x, y):

    return x + y



Пример лямбда-функции, которая выполняет те же действия что и standard_function():



lambda x, y: x + y



Следует обратить внимание, что в общем случае лямбда-функции могут иметь произвольное количество аргументов, как и обычные функции.


Чтобы продемонстрировать, как лямда-функции используются прямо в том месте, где они написаны, ниже дан пример такого использования. Этот пример достаточно бессмысленный, но дает представление как работает лямбда-функция:



a = (lambda x, y: x+y)(5, 7)

print(a)


Результат:


12



Синтаксис описания лямбда-функции, как уже стало понятно из примеров, следующий:



lambda <аргументы функции через запятую> : тело функции



Передача параметров в лямбда-функцию сделана конструкцией (5, 7). То есть, если в классическом вызове обычной функции пишется имя функции и параметры в круглых скобках, то при вызове лямбда-функции пишется сама лямбда, а после нее - параметры в круглых скобках.


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



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



Значит ли это, что лямбда-функции в Python так ограничены, и из-за этих ограничений невозможно делать некоторые алгоритмы? Нет, потому что вместо лямбда-функции всегда можно написать и использовать обычную именованную функцию. Да, это будет более многословно, но по факту ничего принципиально не поменяется.


В синтаксисе написания лямбда-функции так же видна еще одна особенность: в лямбда-функции нет операции return. То есть, нет явной операции возврата вычисленного выражения. В качестве возвращаемого значения выступает значение выражения, записанного в лямбда-функции. Это сделано потому, что тело лямбда-функции состоит только из одного выражения.



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



ourFunction = lambda x, y: x + y

...

a=ourFunction(10, 90)

b=ourFunction(15, 1000)



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


Следует обратить внимание, что в этом примере все так же нет определения обычной функции через стандартную конструкцию def. Функция определена прямо в коде, и присвоена переменной. Такое inline-определение возможно делать именно благодаря механизму лямбда-функций.



Функции обработки списков (контейнеров)


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


Для этих целей хорошо подходит триада функций map(), filter(), reduce().


В качестве входящих обрабатываемых последовательностей обычно используются списки или кортежи.



Функция Map


Встроенная в Python функция map() – это функция, которая позволяет применить некую пользовательскую функцию для всех элементов в последовательности.



В функциональном программировании функция map называется отображением.



Общий формат функции map() следующий:



result = map(пользовательская функция, последовательность)



или



result = map(пользовательская функция, последовательности через запятую)



В качестве пользовательской функции следует указать либо имя обычной функции, либо прямо на этом месте написать лямбда-функцию.


Если обрабатывается одна последовательность, то у пользовательской функции должен быть один параметр. В этот параметр, в момент обработки, будет засовываться значение очередного элемента последовательности.


Если обрабатывается несколько последовательностей, тогда у пользовательской функции должно быть столько параметров, сколько задано входящих последовательностей. В первый параметр пользовательской функции будет вставляться элемент первой последовательности, во второй параметр - элемент второй последовательности и т.д. Итерирование элементов на каждом шаге обработки идет одновременно по всем последовательностям. То есть, на первом шаге в пользовательскую функцию попадут первые элементы всех последовательностей, на втором шаге - вторые элементы последовательностей и т. д.


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



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



Пример обработки одной последовательности:



seq = (1, 2, 3, 4, 5, 6, 7, 8, 9)

result = map(lambda x : x * x + 10, seq)

resultList = list(result)

print(resultList)


Результат:


[11, 14, 19, 26, 35, 46, 59, 74, 91]



Часто map() и list() объединяют в одну конструкцию:



result = list(map(lambda x : x * x + 10, seq))

print(result)



Пример обработки двух последовательностей:



seq = (1, 2, 3, 4, 5, 6, 7, 8, 9)

seq2 = (5, 6, 7, 8, 9, 0, 3, 2, 1)

result = list(map(lambda x, y: x+y, seq, seq2))

print(result)


Результат:


[6, 8, 10, 12, 14, 6, 10, 10, 10]



А вот что будет если одна последовательность окажется короче другой:



seq = (1, 2, 3, 4, 5, 6, 7, 8, 9)

seq2 = (5, 6, 7, 8, 9)

result = list(map(lambda x, y: x+y, seq, seq2))

print(result)


Результат:


[6, 8, 10, 12, 14]



Функция filter()


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


В общем виде функция filter() записывается следующим образом:



result = filter(фильтрующая функция, последовательность)



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


Вот пример кода, который выбирает из последовательности только четные числа:



seq = (1, 2, 3, 4, 5, 6, 7, 8, 9)

filtered = list( filter(lambda x: x % 2 == 0, seq) )

print( filtered )


Результат:


[2, 4, 6, 8]



Функция reduce()


В переводе с английского, слово "reduce" означает "свертка" или "сведение". Имеется в виду, что последовательность элементов сводится по некоторому алгоритму к какому-то одному значению. Для этого и была создана функция reduce().


Функция reduce() обрабатывает последовательность с помощью заданной пользователем функции, возвращая в результате одно-единственное, "накопленное" значение. Результат работы функции reduce() не новая последовательность, а какое-то одно значение. Такое значение ещё можно назвать метрикой.


Какие метрики можно снимать с последовательностей? Например, если последовательность состоит из чисел, можно получить такие метрики:



  • Сумма всех чисел в последовательности
  • Произведение всех чисел в последовательности
  • Среднее арифметическое всех чисел в последовательности
  • Самое большое число в последовательности
  • Самое маленькое число в последовательности
  • Количество четных чисел в последовательности
  • Уникальный хеш последовательности (ну, почти уникальный)
  • Строка, состоящая из ASCII-символов, полученных из чисел последовательности, рассматриваемых как коды символов, в случае если код находится в диапазоне от 65 до 90



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



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


Вторым аргументом пользовательской функции передается значение очередного обрабатываемого элемента.


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



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


Последнее значение, полученное из пользовательской функции, называется сверткой. А сама пользовательская функция - функцией свертки. Функция reduce(), по сути, возвращает свертку, т. е. значение, полученное из пользовательской функции на последней итерации обработки списка.



Примечание: Как видно из всего вышесказанного, пользовательская функция, используемая в reduce(), должна иметь только два аргумента.



Общий формат функции reduce() следующий:



reduce(пользовательская функция, последовательность, начальное значение метрики)



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



numberList = [1, 24, 17, 14, 9, 32, 2]

listMax = reduce(lambda a, b : a if (a > b) else b, numberList, 0)

print(listMax)



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



sentences = ["Варкалось.",

"Хливкие шорьки пырялись по наве,",

"и хрюкотали зелюки, как мюмзики в мове."]


wordCountFunction = lambda count, text : count + len(text.split())


numberOfWords = reduce(wordCountFunction, sentences, 0)


print(numberOfWords)



Здесь в пользовательской функции первый аргумент - count - является накопителем, а второй аргумент - text - является текстом очередной обрабатываемой строки. Начальное значение count задается в третьем аргументе функции reduce().


* * *


На этом беглый обзор работы функций map/filter/reduce можно закончить. На самом деле, концепция обработки последовательностей через функции довольно проста, и используется во многих языках программирования, имея в каждом свои особенности. В этой статье рассказано об элементах функционального программирования в языке Python, однако такие же приемы можно использовать и в других развитых языках.


Так же в этом разделе:
 
MyTetra Share v.0.65
Яндекс индекс цитирования