image

Я решил написать эту серию статей, ибо считаю, что никто не должен сталкиваться с той стеной непонимания, с которой столкнулся когда-то я.

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

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


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

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

Давайте кратко разберемся в этих двух понятиях прежде чем продолжим.

Чистая Функция — Функция которая является детерминированной и не обладает никакими побочными эффектами.

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

Пример детерминированной функции

def sum(a,b):
  return a+b

print(sum(3,6))
print(sum(3,6))
print(sum(3,6))

>>>9
>>>9
>>>9

И пример не детерминированной:

from datetime import date

todays_weekday = date.today().weekday()
def sum(a,b):
  if todays_weekday == 1:
    result = a+b
    print(f'sum is {result} and result class is {type(result)}')
  else:
    result = str(a+b)
    print(f'sum is {result} and result class is {type(result)}')

sum(4,5)
todays_weekday = 4
sum(4,5)

>>>sum is 9 and result class is <class 'int'>
>>>sum is 9 and result class is <class 'str'>

Каждый раз при смене дня недели (который не является аргументом функции) функция выдает разные результаты.

Самый очевидный пример не детерминированной функции это random:

import random
print(random.random())
print(random.random())
print(random.random())

>>>0.5002395668892657
>>>0.8837128676460416
>>>0.5308851462814731

Второе важное качество чистой функции это отсутствие побочных эффектов.

Покажем наглядно:

my_list = [32,3,50,2,29,43]

def sort_by_sort(li):
  li.sort()
  print(li)

sort_by_sort(my_list)
print(my_list)

>>>[2, 3, 29, 32, 43, 50]
>>>[2, 3, 29, 32, 43, 50]

Функция sort_by_sort имеет побочные эффекты потому что изменяет исходный список элементов и выводит что то в консоль.

my_list = [32,3,50,2,29,43]

def sort_by_sorted(li):
  return sorted(li)

print(sort_by_sorted(my_list))
print(my_list)

>>>[2, 3, 29, 32, 43, 50]
>>>[32, 3, 50, 2, 29, 43]

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

Чистые функции хороши тем что:

  • Они проще читаются
  • Они проще поддерживаются
  • Они проще тестируются
  • Они не зависят от того в каком порядке их вызывать

Функциональное программирование можно охарактеризовать тем что в нем используются только чистые функции.

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

Пример ФВП:

def func(num):
  return num**2

def higher_order_func(fun, num):
  return fun(num)+fun(num)

print(func(4))
print(higher_order_func(func,4))

>>>16
>>>32

С основами чуть чуть разобрались и теперь перейдем к следующему шагу.

Итак, начнем


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

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

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

Как говорил мой любимый учитель 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 а нам это будет неудобно

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

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

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


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

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


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

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 а нам это будет неудобно

print((lambda height, radius : (height*pi_const*(radius**2))/3)(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}
]

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

print('-------------')

# Сортируем по датам
for book in sorted(asoiaf_books, key=lambda book: book.get('published')):
  print(book)

print('-------------')

# Сортируем по количеству страниц
for book in sorted(asoiaf_books, key=lambda book: book.get('pages')):
  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: По многочисленным просьбам, расставил знаки препинания.