habrahabr

Функциональное программирование на Python для самых маленьких — Часть 1 — Lambda Функция

  • вторник, 23 июня 2020 г. в 00:32:11
https://habr.com/ru/post/507642/
  • Python
  • Функциональное программирование


image

Я решил написать эту серию статей, ибо считаю, что никто не должен сталкиваться с той стеной непонимания, с которой столкнулся когда-то я.
Ведь большинство статей написаны таки образом что, для того чтобы понять что-то в Функциональном Программировании (далее ФП), тебе надо уже знать многое в ФП. Эту статью я старался написать максимально просто — настолько понятно, чтобы её суть мог уловить мой племянник, школьник, который сейчас делает свои первые шаги в Python.

Небольшое введение


Для начала, давайте разберемся, что такое функциональное программирование, в чем его особенности, зачем оно было придумано, а также где и как его использовать. Стоп… А зачем? Об этом написаны тонны материалов, да и в этой статье судя по всему эта информация не особо нужна. Эта статья написана для того, чтобы читатели научились разбираться в коде, который написан в функциональном стиле. Но если вы все-таки хотите разобраться в истории Функционального Программирования и в том, как оно работает под капотом, то советую вам почитать о таких вещах, как

  • Чистая Функция
  • Функции высшего порядка

Но для понимания того, что сказано в этой статье, знать это не обязательно.

Итак, начнем


Для начала надо понять следующее — что такое Функциональное Программирование вообще. Лично я знаю две самые часто упоминаемые парадигмы в повседневном программировании — это ООП и ФП.

Если упрощать совсем и объяснять на пальцах, то описать эти две парадигмы можно следующим образом:

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


Как говорил мой любимый учитель zverok Виктор Шепелев: «Вся работа в программировании — это работа с данными. Взял какие-то данные, поигрался с ними и вернул обратно.»


Это относится и к ФП — взял какие-то данные, взял какую-то функцию, поигрался с ними и выдал что-то на выходе.

Не стану расписывать всё, иначе это будет оооочень долго. Цель данной статьи — помочь разобраться, а не объяснить, как и что работает, поэтому тут мы рассмотрим основные функции из ФП.

В большинстве своем ФП (как я его воспринимаю) — это просто упрощенное написание кода. Любой код, написанный в функциональном стиле, может быть довольно легко переписан в обычном стиле без потери качества, но более примитивно. Цель ФП заключается в том, чтобы писать код более простой, понятный и который легче поддерживать, а также который занимает меньше памяти, ну и куда же без этого — разумеется, главная вечная мораль программирования — DRY (Don’t Repeat Yourself — Не повторяйся).

Сейчас мы с вами разберем одну из основных функций, которая применяется в ФП — Lambda функцию.

В следующих статьях мы разберем такие функции как Map, Zip, Filter и Reduce.

Lambda функция


Lambda — это инструмент в python и других языках программирования для вызова анонимных функций. Многим это скорее всего ничего не скажет и никак не прояснит того, как она работает, поэтому я расскажу вам просто механизм работы lambda выражений.

Все очень просто.

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

Формула площади круга это

S = pi*(r**2)

где
S — это площадь круга
pi — математическая константа равная 3.14 которую мы получим из стандартной библиотеки Math
r — радиус круга — единственная переменная которую мы будем передавать нашей функции

Круг с радиусом

Теперь оформим это все в python

import math #Подключаем библиотеку math

pi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно

# Пишем функцию которая будет вычислять площадь круга по заданному радиусу в обычном варианте записи
def area_of_circle_simple(radius):
  return pi_const*(radius**2)

print(area_of_circle_simple(5))
print(area_of_circle_simple(12))
print(area_of_circle_simple(26))
>>>78.5
>>>452.16
>>>2122.64


Вроде бы неплохо, но это всё может выглядеть куда круче, если записывать это через lambda

import math #Подключаем библиотеку math

pi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно

# Пишем функцию которая будет вычислять площадь круга по заданному радиусу в функциональном стиле
area_of_circle_by_lambda = lambda radius: pi_const*(radius**2) 

print(area_of_circle_by_lambda(5))
print(area_of_circle_by_lambda(12))
print(area_of_circle_by_lambda(26))

>>>78.5
>>>452.16
>>>2122.64

Для полноты картины покажем, как вызывать lambda функцию анонимно

import math #Подключаем библиотеку math

pi_const = round(math.pi, 2) #округляем pi до второго знака 
# после запятой иначе она будет выглядеть 
# как 3.141592653589793 а нам это будет неудобно

print((lambda radius: pi_const*(radius**2))(5))
print((lambda radius: pi_const*(radius**2))(12))
print((lambda radius: pi_const*(radius**2))(26))

>>>78.5
>>>452.16
>>>2122.64

Чтобы было понятнее, анонимный вызов функции подразумевает то, что вы используете её, нигде не объявляя, как в примере выше.

Лямбда функция работает по следующему принципу

func = lambda перечисляются аргументы через запятую : что то с ними делается

print(func(передаем аргументы))

>>>получаем результат того что находится после двоеточия двумя строками выше


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

V = (height*pi_const*(radius**2))/3

Конус с габаритами

Запишем это все в python:

import math #Подключаем библиотеку math

pi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно

#Формула объема конуса в классической форме записи
def cone_volume(height, radius):
  volume = (height*pi_const*(radius**2))/3
  return volume

print(cone_volume(3, 10))

>>>314.0

А теперь как это будет выглядеть в lambda форме:

import math #Подключаем библиотеку math

pi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно

#Формула объема конуса в лямбда записи
cone_volume = lambda height, radius : (height*pi_const*(radius**2))/3

print(cone_volume(3, 10))

>>>314.0


Количество переменных здесь никак не ограничено. Для примера посчитаем объем усеченного конуса, где у нас учитываются 3 разные переменные.

Объем усеченного конуса считается по формуле:

V = (pi_const*height*(r1**2 + r1*r2 + r2**2))/3

Усеченный конус с габаритами

И вот, как это будет выглядеть в python классически

import math #Подключаем библиотеку math

pi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно

#Формула объема усеченного конуса в классической записи
def cone_volume(h,r1,r2):
  return (pi_const * h * (r1 ** 2 + r1 * r2 + r2 ** 2))/3

print(cone_volume(12, 8, 5))
print(cone_volume(15, 10, 6))
print(cone_volume(20, 12, 9))

>>>1620.24
>>>3077.20
>>>6970.8


А теперь покажем, как это будет выглядеть с lambda, но при этом не будем объявлять функцию заранее, а опишем ее в момент вывода:

import math #Подключаем библиотеку math

pi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно

print((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(12, 8, 5))
print((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(15, 10, 6))
print((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(20, 12, 9))

>>>1620.24
>>>3077.20
>>>6970.8


После того, как мы разобрались, как работает lambda функция, давайте разберем ещё кое-что интересное, что можно делать с помощью lambda функции, что может оказаться для вас весьма неожиданным — Сортировку

Сортировать одномерные списки в python с помощью lambda довольно глупо — это будет выглядеть, как бряцание мускулами там, где оно совсем не нужно.
Ну серьезно допустим, у нас есть обычный список (не важно состоящий из строк или чисел) и нам надо его отсортировать — тут же проще всего использовать встроенную функцию sorted()
И в правду, давайте посмотрим на это

new_int_list = [43,23,56,75,12,32] # Создаем список чисел
print(sorted(new_int_list)) # Сортируем список чисел
new_string_list = ['zum6z', 'yybt0', 'h1uwq', '2k9f9', 'hin9h', 'b0p0m'] # Создаем список строк
print(sorted(new_string_list)) # Сортируем список строк

>>>[12, 23, 32, 43, 56, 75]
>>>['2k9f9', 'b0p0m', 'h1uwq', 'hin9h', 'yybt0', 'zum6z']


В таких ситуациях, действительно, хватает обычного sorted() (ну или sort(), если вам нужно изменить текущий список на месте без создания нового, изменив исходный)

Но что, если нужно отсортировать список словарей по разным ключам? Тут может быть запись как в классическом стиле, так и в функциональном. Допустим, у нас есть список книг вселенной Песни Льда и Пламени с датами их публикаций и количеством страниц в них.
Как всегда, начнем с классической записи.

# Создали список из словарей книг
asoiaf_books = [
  {'title' : 'Game of Thrones', 'published' : '1996-08-01', 'pages': 694},
  {'title' : 'Clash of Kings', 'published' : '1998-11-16', 'pages': 761},
  {'title' : 'Storm of Swords', 'published' : '2000-08-08', 'pages': 973},
  {'title' : 'Feast for Crows', 'published' : '2005-10-17', 'pages': 753},
  {'title' : 'Dance with Dragons', 'published' : '2011-07-12', 'pages': 1016}
]

# Функция по получению названия книги
def get_title(book):
    return book.get('title')

# Функция по получению даты публикации книги
def get_publish_date(book):
    return book.get('published')

# Функция по получению количества страниц в книге
def get_pages(book):
    return book.get('pages')

# Сортируем по названию
asoiaf_books.sort(key=get_title)
for book in asoiaf_books:
  print(book)
print('-------------')
# Сортируем по датам
asoiaf_books.sort(key=get_publish_date)
for book in asoiaf_books:
  print(book)
print('-------------')
# Сортируем по количеству страниц
asoiaf_books.sort(key=get_pages)
for book in asoiaf_books:
  print(book)

>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}
>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}
>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}
>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}
>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}
>>>-------------
>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}
>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}
>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}
>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}
>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}
>>>-------------
>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}
>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}
>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}
>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}
>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}

А теперь перепишем это все через lambda функцию

# Создали список из словарей книг
asoiaf_books = [
  {'title' : 'Game of Thrones', 'published' : '1996-08-01', 'pages': 694},
  {'title' : 'Clash of Kings', 'published' : '1998-11-16', 'pages': 761},
  {'title' : 'Storm of Swords', 'published' : '2000-08-08', 'pages': 973},
  {'title' : 'Feast for Crows', 'published' : '2005-10-17', 'pages': 753},
  {'title' : 'Dance with Dragons', 'published' : '2011-07-12', 'pages': 1016}
]

# Сортируем по названию
asoiaf_books.sort(key=lambda book: book.get('title'))
for book in asoiaf_books:
  print(book)
print('-------------')

# Сортируем по датам
asoiaf_books.sort(key=lambda book: book.get('published'))
for book in asoiaf_books:
  print(book)
print('-------------')
# Сортируем по количеству страниц
asoiaf_books.sort(key=lambda book: book.get('pages'))
for book in asoiaf_books:
  print(book)

>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}
>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}
>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}
>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}
>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}
>>>-------------
>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}
>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}
>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}
>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}
>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}
>>>-------------
>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}
>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}
>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}
>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}
>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}

Таким образом, lambda функция хорошо подходит для сортировки многомерных списков по разным параметрам.

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

Но где же тут та самая экономия места, времени и памяти? Экономится максимум пара строк.

И вот тут мы подходим к реально интересным вещам.

Которые разберем в следующей статье, где мы обсудим map функцию.

UPD: По многочисленным просьбам, расставил знаки препинания