Функциональное программирование в Python
Функциональное программирование в Python

Python и функциональное программирование

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

ФП основывается на взаимодействии с функциями. Функции, функции everywhere!

Функциональное программирование и его преимущества

Функциональная программа – это совокупность определений функций.

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

Вычисления начинаются с вызова некоторой функции. Она, в свою очередь, тоже вызывает функции, которые входят в её определение в соответствии с внутренней иерархией (часто вызовы происходят рекурсивно).

my_list = list(map(lambda x, y: x + y, [1, 2], [3, 4])) print(my_list) > [4, 6]

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

Функциональное программирование часто определяют как программирование, в котором нет побочных эффектов

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

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

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

К преимуществам такого способа написания кода можно отнести:

  • Надёжность – отсутствие побочных эффектов, исключение мутации данных и отсутствие ошибок, вроде случайного присваивания неверного типа переменной – всё это повышает надежность программы.
  • Лаконичность – в большинстве случаев, программа, написанная в функциональном стиле, будет на порядок короче аналогичного кода с применением других парадигм. Некоторые разработчики отмечают, что код ФП-программ ещё и более понятный. Но это чистый субъективизм.
  • Удобство тестирования – функциональная программа – мечта юнит-тестера. Так как функции не производят побочных эффектов, не меняют объекты и всегда возвращают строго детерминированный результат – тестировать их легко и просто. Всего лишь нужно вычислить значение функции на разных наборах аргументов.
  • Оптимизация – ФП позволяет писать код в декларативном стиле. Таким образом, последовательность выполнения операций в явном виде не задаётся, а автоматически синтезируется в ходе вычислений. Это даёт возможность применять сложные методы автоматической оптимизации.
  • Параллелизм – ФП по определению гарантирует отсутствие побочных эффектов. А в любом вызове функции всегда допустимо параллельное вычисление двух различных параметров. Причём порядок их вычисления не влияет на результат вызова.

def vs лямбда-выражение

А как там у нас в Питоне с ФП? Всё отлично. Python – современный язык, который полностью поддерживает парадигму функционального программирования.

Функция в Python может быть определена через классический оператор def:

def add_one (a, b): return a + b + 1

А можно эту запись заменить эквивалентным лямбда-выражением:

add_one = lambda a, b: a + b + 1

Это второй способ определения функций.

Встроенные функции высших порядков

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

map()

Принимает функцию-аргумент и применяет её ко всем элементам входящей последовательности.

# напечатаем квадраты чисел от 1 до 5 my_list = list(map(lambda x: x**2, [1, 2, 3, 4, 5])) print(my_list) > [1, 4, 9, 16, 25]

filter()

Как следует из названия, filter() фильтрует последовательность по заданному условию.

# отфильтруем список с целью получить только чётные значения my_list = list(filter(lambda x: x % 2 == 0, [11, 22, 33, 44, 55, 66])) print(my_list) > [22, 44, 66]

apply()

Применяет входящую функцию к позиционным и именованным аргументам. Эти аргументы задаются списком и словарём соответственно.

💁‍♀️ В Python 3 вместо имени функции применяется специальный синтаксис:

def print_things(q, w, e, r, t=None, y=1): print(q, w, e, r, t, y) print_things(*[0, 2, 4, 6], **{'t': 8, 'y': 10}) > 0 2 4 6 8 10

zip()

Упаковывает итерируемые объекты в один список кортежей. При работе ориентируется на объект меньшей длины:

word_list = ['Elf', 'Dwarf', 'Human'] digit_tuple = (3, 7, 9, 1) ring_list = ['ring', 'ring', 'ring', 'ring', 'ring'] my_list = list(zip(word_list, digit_tuple, ring_list)) print(my_list) > [('Elf', 3, 'ring'), ('Dwarf', 7, 'ring'), ('Human', 9, 'ring')]

Модуль functools

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

reduce()

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

from functools import reduce # посчитаем сумму элементов списка res = reduce(lambda sum, x: sum + x, [0.1, 0.3, 0.6]) print(res) > 1.0

С помощью reduce() можно делать так:

import functools # посчитаем с reduce количество вхождений строки в список uncle_ben = ['С большой силой', 'приходит', 'большая ответственность'] complete = functools.reduce(lambda a, x: a + x.count('ответственность'), uncle_ben, 0) print(complete) > 1

partial

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

import functools # допустим у нас есть какая-то фукнция def set_stars(amount, type): print(type, amount) # уменьшаем число аргументов set_stars с partial print_neutron_star = functools.partial(set_stars, type='Neutron stars: ') print_neutron_star(1434) > Neutron stars: 1434

cmp_to_key

Возвращает ключевую функцию для компарации объектов.

# сортировка по убыванию def num_compare(x,y): return y - x print(sorted([4, 43, 1, 22], key=functools.cmp_to_key(num_compare))) > [43, 22, 4, 1]

lru_cache

Это – декоратор. То есть "обёртка", которая может изменить поведение функции, не меняя её код. Lru_cache даёт выбранной функции кэширование, чтобы фиксировать результаты тяжеловесных вычислений, запросов или других операций.

from functools import lru_cache # функция ищет num-е число Фибоначчи @lru_cache(maxsize=None) def fib_rec(num): if num < 2: return num return fib_rec(num - 1) + fib_rec(num - 2) print(fib_rec(100)) print(fib_rec.cache_info()) >354224848179261915075 > CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)

total_ordering

Это декоратор класса, в котором задаются методы сравнения.

update_wrapper

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

wraps

wraps – это ещё один декоратор. Он применяется к функции-обертке. wraps обновляет функцию-обертку так, чтобы она выглядела как оборачиваемая функция. При этом копируются атрибуты __name__ и __doc__.

Замыкания

Замыкания неразрывно связаны с концепцией вложенных функций. Это такие функции, в теле которых есть ссылки на переменные, объявленные вне определения этой функции и не являющиеся её параметрами. Можно сказать, что функция запоминает свои внешние переменные и может получить к ним доступ. JS-разработчики сталкиваются с замыканиями ежедневно.

def substractor(f_num): def sub(s_num): return s_num - f_num return sub # функция, которая отнимает 5 из аргумента sub_five = substractor(5) print(sub_five(10)) > 5 # аналог substractor = lambda f_num: lambda s_num: s_num - f_num sub_four = substractor(4) print(sub_four(8)) > 4

Итераторы

Python 3 itertools позволяет создавать собственные итераторы, которые работают быстрее и более оптимально используют память.

Доступные итераторы библиотеки itertools:

  • accumulate();
  • chain();
  • chain.from_iterable();
  • compress();
  • dropwhile();
  • filterfalse();
  • groupby();
  • islice();
  • starmap();
  • takewhile();
  • tee();
  • zip_longest().

Все вышеперечисленные итераторы конечны. Но в модуле также представлено три бесконечных итератора. При их использовании не забываете про break.

  1. count();
  2. cycle();
  3. repeat().

Наконец в модуле есть трио комбинаторных генераторов;

  1. combinations();
  2. combinations_with_replacement();
  3. product().

Таким образом, библиотека itertools – это мощнейший инструмент для создания и использования итераторов, о котором студенты, изучающие C++, могут лишь мечтать!

Ленивые вычисления

Такое название неспроста. В процессе lazy evaluations вычисления происходят лишь тогда, когда это нужно, а не заранее. Такой подход позволяет оперировать с бесконечными объектами. Расчёт происходит не постоянно, и в определенный момент времени вычисленными будут только те элементы, которые нужны прямо сейчас. Если возникнет необходимость в получении других элементов этого объекта, то они тоже будут вычислены и сохранены.

import evalcache # всё то вычисление чисел Леонарда Пизанского, но теперь «ленивое» lazy = evalcache.Lazy(cache={}, onuse=True) @lazy def lazy_fib(n): if n < 2: return n return lazy_fib(n - 1) + lazy_fib(n - 2)

Искусственный интеллект, big data и блокчейны последние пять лет переживают эпоху взрывного роста. В связи с этим, перед программистами встала проблема больших потоков данных. Параллельные и децентрализованные вычисления позволяют увеличить скорость обработки big data на порядки. Однако когда речь идёт о параллелизме, ООП внезапно спотыкается. Знакомая нам парадигма уже не может предложить разработчикам и пользователям оптимальную скорость, в то время как функциональный код справляется с этой задачей на ура.

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

К примеру, штат из 55 сотрудников Facebook, используя функциональный Erlang, способен поддерживать 2 миллиарда пользователей WhatsApp-а. Данных с каждым днём становится всё больше, поэтому популярность ФП будет только расти, и этот рост уже хорошо прослеживается по числу релевантных вакансий на сайтах с вакансиями.

😭
😕
😃
😍