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]
update_wrapper()
Используется для обновления метаданных функции-обертки, данными из некоторых атрибутов оборачиваемой функции. Обеспечивает лучшую читаемость и возможность повторного использования кода.
@lru_cache
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
Это декоратор класса, в котором задаются методы сравнения.
@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
.
- count();
- cycle();
- repeat().
Наконец в модуле есть трио комбинаторных генераторов;
- combinations();
- combinations_with_replacement();
- 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-а. Данных с каждым днём становится всё больше, поэтому популярность ФП будет только расти, и этот рост уже хорошо прослеживается по числу релевантных вакансий на сайтах с вакансиями.
Спасибо большое за информацию.