habrahabr

Учим функциональное программирование в Python за 10 минут

  • суббота, 10 августа 2019 г. в 00:20:02
https://habr.com/ru/post/456814/
  • Python
  • Программирование
  • Функциональное программирование


image
Фото: Chris Ried

В этой статье вы поймете, что такое функциональная парадигма и как пользоваться функциональным программированием в Python. Также узнаете об абстракции списков и других способах абстракции (list comprehensions).

Функциональная парадигма


В императивной парадигме вы пишете программу путем указания последовательности действий, которые позже выполняются. В это время состояния (прим. переводчика: переменных, массивов, и т.д.) меняются. Например, пусть переменная А хранит значение 5, позже Вы изменяете значение этой переменной. Вы пользуетесь переменными так, что их значения меняются.

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

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

a = 3
def some_func():
    global a
    a = 5

some_func()
print(a)

Результат исполнения этого кода равен 5. В функциональном программировании изменение переменных находится под запретом и изменение функциями чего-либо за своими границами тоже. Все что может функция это посчитать/обработать что-то и вернуть результат.

Сейчас, Вы возможно думаете: «Никаких переменных, никаких побочных эффектов? Почему это хорошо?» Действительно хороший вопрос.

Если функция была вызвана дважды с одинаковыми параметрами, очевидно она возвратит тот же результат. Если Вы изучали что-то о математических функциях, то Вы оцените эту возможность. Это называется прозрачностью ссылок или чистотой языка программирования (referential transparency). Поскольку функции не имеют побочных эффектов, если Вы разрабатываете программу для обсчетов, Вы можете ускорить процесс выполнения. Если программе известно, что func(2) равняется 3, мы можем это запомнить. Это предотвращает повторный вызов функции, когда мы уже знаем результат.

Обычно, в функциональном программировании, циклы не используются. Используется рекурсия. Рекурсия это математический концепт, по сути, он означает «скармливание что-то самому себе». В рекурсивной функции, сама функция вызывает себя же в роли под-функции. Приведем пример рекурсивной функции в Python:

def factorial_recursive(n):
    # Base case: 1! = 1 # Основной случай
    if n == 1:
        return 1

    # Recursive case: n! = n * (n-1)! # Тут случается рекурсия
    else:
        return n * factorial_recursive(n-1)

Некоторые языки программирования ленивы. Это означает, что они все обсчитывают в последний момент. Допустим если в коде должно выполниться 2+2, функциональная программа посчитает результат только тогда, когда будет нужен результат. Узнаем про ленивости Python немного позже.

Map


Чтобы понять map, нужно сначала разобраться с итерируемыми контейнерами. Это такой контейнер, по которому можно «пробежаться». Это зачастую списки или массивы, но в Python таких контейнеров много. Можно даже создать свой контейнер, введя магические методы. Эти методы как API, которе помогают объектам стать более питоническими. Таких методов нужно 2, чтобы сделать объект итерируемым:

class Counter:
    def __init__(self, low, high):
        # set class attributes inside the magic method __init__
        # for "inistalise"
        # Задаем атрибуты класса для инициализации
        self.current = low
        self.high = high

    def __iter__(self):
        # first magic method to make this object iterable
        # Первый магический метод
        return self
    
    def __next__(self):
        # second magic method
        # Второй магический метод
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
return self.current - 1

Первый магический метод это "___iter__" или dunder (дважды выделенный подчеркиваниями ) iter возвращает итерируемый объект, это часто используется в начале цикла. Dunder next (__next__) возвращает следующий объект.

Проверим это:

for c in Counter(3, 8):
print(c)

Результат исполнения:

3
4
5
6
7
8


В Python, итератор это объект, который имеет только метод __iter__. Это означает, что Вы можете получить доступ к месту ячеек объекта (контейнера), но не сможете «пройтись» по ним. Некоторые объекты имеют только чудный метод __next__, без волшебного метода __iter__, например set (о нём позже). В этой статье мы покроем все, что касается итерируемых объектов.

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

map(function, iterable)

Допустим у нас есть список с такими элементами:

[1, 2, 3, 4, 5]

И мы хотим возвести в квадрат каждый элемент, это можно сделать так:

x = [1, 2, 3, 4, 5]
def square(num):
    return num*num

print(list(map(square, x)))

Функциональные функции в Python ленивы. Если мы не добавим «list()», функция будет хранить описание контейнера (списка), а не сам список. Нам непосредственно нужно сказать Pythonону, чтоб он конвертировал это в список.

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

Писать функции, например, «square(num)» это нормально, но не совсем правильно. Нам нужно объявить целую функцию, чтобы только использовать её в map? Можно упростить это дело путём введения (анонимных) lambda функций.

Lambda выражения


Lambda выражения это функции в одну строчку, например, вот lambda выражение, которое возводит в квадрат полученное число:

square = lambda x: x * x

И, запустим это:

>>> square(3)
9


Я вас слышу. «Брендон, где аргументы? Что это вообще такое? Это не похоже на функцию.»

Да, это может сбить с толку, но это можно объяснить. В этой строчке мы присваиваем кое-что переменной «square». Эта часть:

lambda x: x * x

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

Чтобы наша программа приняла вид one-line, нужно сделать так:

x = [1, 2, 3, 4, 5]
print(list(map(lambda num: num * num, x)))

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

Reduce


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

reduce(function, list)

Мы можем (и часто будем) использовать lambda функции в роли function аргумента.

Если мы хотим перемножить все числа в списке, это можно сделать так:

product = 1
x = [1, 2, 3, 4]
for num in x:
product = product * num

А с reduce это будет выглядеть так:

from functools import reduce

product = reduce((lambda x, y: x * y),[1, 2, 3, 4])

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

Filter


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

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

Синтаксис:

filter(function, list)

Посмотрим на пример без использования filter:

x = range(-5, 5)
new_list = []

for num in x:
    if num < 0:
new_list.append(num)

Вместе с filter:

x = range(-5, 5)
all_less_than_zero = list(filter(lambda num: num < 0, x))

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


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


def summation(nums):
    return sum(nums)

def action(func, numbers):
    return func(numbers)

print(action(summation, [1, 2, 3]))

# Output is 6 # Вывод 6

Или пример ещё проще, это:

def rtnBrandon():
    return "brandon"
def rtnJohn():
    return "john"

def rtnPerson():
    age = int(input("What's your age?"))

    if age == 21:
        return rtnBrandon()
    else:
        return rtnJohn()

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

Все функции в Python это объекты первого класса. Объект первого класса определён как таковой, что соответствует одному или более таких параметров:

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

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

Частичное применение


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

def power(base, exponent):
  return base ** exponent

Теперь нам нужно создать отдельную функцию для возведения в квадрат, и вычислить его используя функцию power:

def square(base):
  return power(base, 2)

Это работает, но что если мы хотим возвести число в куб? Или в 4-ую степень? Предстоит писать такие функции вечно? Конечно, можно. Но программисты ленивы. Если Вы повторяете одну и ту же вещь несколько раз, наверняка есть способ сделать это быстрее и перестать делать повторения. Здесь можно использовать частичное применение. Посмотрим на пример функции power c использованием частичного применения:

from functools import partial

square = partial(power, exponent=2)
print(square(2))

# output is 4 # вывод 4

Разве не круто? Мы можем вызвать функцию которой нужно 2 аргумента, используя только 1, и указав каким будет второй самостоятельно.

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

from functools import partial

powers = []
for x in range(2, 1001):
  powers.append(partial(power, exponent = x))

print(powers[0](3))
# output is 9 # вывод 9

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


Вы могли заметить, что много вещей которые мы хотим сделать в функциональном программировании вертятся вокруг списков. Помимо функции reduce и частичного применения, все функции которые Вы видели генерируют списки. Гвидо (создатель Python`a) не любит функциональные вещи в Python`e, так как у Python`a есть свой метод создания списков.

Если Вы напишете “import this” в консоли, то получите:
>>> import this

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one — and preferably only one — obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea — let’s do more of those!


Это Python-Дзен. Это стих о том что значит быть питонистом. Часть которая нас интересует это:

There should be one  —  and preferably only one  —  obvious way to do it.

Должен быть только один — и предпочтительно только один — очевидный путь что-то сделать

В Python`e, map и filter могут делать то же самое что и абстракция списков (ссылка). Это нарушает одно из правил Python-Дзена, так что эта часть функционального программирования не «питоническая».

Следующие о чём стоит поговорить это lambda-функция. В Python`e, lambda-функция это нормальная функция. И по сути это синтаксический сахар. Обе эти части делают одно и то же:


 foo = lambda a: 2

 def foo(a):
   return 2

Стандартная функция может всё то же, что и lambda-функция, но не наоборот. Lambda-функция не может того же, что может обычная.

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

Абстракция списков


Я уже сказал, что все что можно сделать с помощью map и filter, можно сделать при помощи абстракции списков. В этой части мы это обсудим.

Абстракция списков это способ создания списков в Python. Синтаксис:

[function for item in iterable]

Давайте возведем в квадрат каждый элемент списка, пример:

print([x * x for x in [1, 2, 3, 4]])

Ладно, мы можем увидеть как применить функцию к каждому элементу списка. Каким же образом мы вертимся вокруг filter? Глянем на этот код:

x = range(-5, 5)

all_less_than_zero = list(filter(lambda num: num < 0, x))
print(all_less_than_zero)

А теперь используем абстракцию списков:

x = range(-5, 5)

all_less_than_zero = [num for num in x if num < 0]

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

Что если мы хотим возвести в квадрат каждый элемент списка, который ниже нуля. С lambda-функцией, map и filter это будет выглядеть так:

x = range(-5, 5)

all_less_than_zero = list(map(lambda num: num * num, list(filter(lambda num: num < 0, x))))

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

x = range(-5, 5)

all_less_than_zero = [num * num for num in x if num < 0]

Абстракция списков это хорошо только, как не странно, для списков. Map и filter работают для каждого итерируемого контейнера, так что же не так?.. Да, можно использовать абстракцию для каждого итерируемого контейнера который Вы встретите.

Другие абстракции


Можно применить абстракцию для каждого итерируемого контейнера.

Каждый итерируемый контейнер может быть создан при помощи абстракции. Начиная с версии 2.7, можно даже создать словарь (хеш-таблицу).

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

  • Set это контейнер элементов, элементы в нём не повторяются
  • Порядок не важен

# taken from page 87, chapter 3 of Fluent Python by Luciano Ramalho
# Взято из книги Fluent Python, стр. 87, п. 3

>>> from unicodedata import name
>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), '')}
{'×', '¥', '°', '£', '', '#', '¬', '%', 'µ', '>', '¤', '±', '¶', '§', '<', '=', '', '$', '÷', '¢', '+'}

Как Вы могли заметить set, как и словарь (dictionary) использует фигурные скобки. Python действительно умный. Он догадается используете Вы абстракцию словаря или же абстракцию set`a, основываясь на том, задаёте ли Вы дополнительные параметры для словаря или нет. Если Вы хотите узнать больше об абстракциях, прочтите эту. Если же об абстракциях и генерировании, то эту.

Итог


Функциональное программирование прекрасно. Функциональный код может быть как чистым, так и не очень. Некоторые хардкорщики-питонисты не принимают функциональную парадигму в Python`e. Вы должны использовать то, что Вы хотите и то, что вам подходит.

Страничка автора.