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

Паради́гма программи́рования — это совокупность идей и понятий, определяющих стиль написания компьютерных программ (подход к программированию). Это способ концептуализации, определяющий организацию вычислений и структурирование работы, выполняемой компьютером. Вики

Функциона́льное программи́рование — парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании).

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

Функции в Python — это такие же объекты, как и, например, строки, списки или классы. Их можно передавать в другие функции, возвращать из функций, создавать на лету — то есть это объекты первого класса (могут быть переданы как параметр, возвращены из функции, присвоены переменной).

Функциональный код отличается одним свойством: отсутствием побочных эффектов. Он не полагается на данные вне текущей функции, и не меняет данные, находящиеся вне функции. Следовательно:

  1. Надёжность кода
  2. Удобство тестирования.
  3. Параллелизм и возможность оптимизации
def caller(func, params):
    return func(*params)


def printer(name, origin):
    print('I\'m {} of {}!'.format(name, origin))

caller(printer, ['Moana', 'Motunui'])
I'm Moana of Motunui!

Концепции проектирования функций

  1. Взаимодействие: для передачи значений функции используйте аргументы, для возврата результатов – инструкцию return. Всегда следует стремиться сделать функцию максимально независимой от того, что происходит за ее пределами.
  2. Взаимодействие: используйте глобальные переменные, только если это действительно необходимо.
  3. Взаимодействие: не воздействуйте на изменяемые аргументы, если вызывающая программа не предполагает этого.
  4. Связность: каждая функция должна иметь единственное назначение.
  5. Размер: каждая функция должна иметь относительно небольшой размер.
def extender(source_list, extend_list):
    result_list  = source_list.copy()
    result_list.extend(extend_list)
    return result_list


values = [1, 2, 3]
print(extender(values, [4, 5, 6]), values)
[1, 2, 3, 4, 5, 6]

Замыкания

Замыкание (англ. closure) в программировании — функция первого класса, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся её параметрами.

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

def make_adder(n):
    def adder(m):
        return m + n
    return adder

add5_f = make_adder(5) # "functional"
add5_f(10)
15
add5_f(10)
15

map

Иногда бывает необходимо применить какую-то функцию к набору элементов. Для этих целей существует несколько стандартных функций. Одна из таких функций — это map, которая принимает функцию и какой-то итерабельный объект (например, список) и применяет полученную функцию ко всем элементам объекта.

def squarify(a):
    return a**2


list(map(squarify, range(5)))
[0, 1, 4, 9, 16]

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

Попробуйте переписать эту программу в функциональном стиле:

names = ['Mary', 'Isla', 'Sam']

for i in range(len(names)):
    names[i] = hash(names[i])

print(names)
[-7256763512443619074, -3535677004771872435, 6581327586727911380]

filter

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

def is_positive(a):
    return a > 0


list(filter(is_positive, range(-2, 3)))
[1, 2]

lambda-функции

Если мы хотим передать куда-либо небольшую функцию, которая нам больше не понадобится, можно использовать анонимные функции (или lambda-функции). Lambda позволяет вам определить функцию in place, то есть без литерала def.

Общая форма:

lambda argument1, argument2,... argumentN : выражение, использующее аргументы

lambda – это выражение, а не инструкция. По этой причине ключевое слово lambda может появляться там, где синтаксис языка Python не позволяет использовать инструкцию def.

Сделаем то же самое, что и в предыдущем примере, c помощью lambda:

list(map(lambda x: x ** 2, range(5)))
[0, 1, 4, 9, 16]
list(filter(lambda x: x > 0, range(-2, 3)))
[1, 2]

Модули для функционального программирования

reduce

Модуль functools позволяет использовать функциональные особенности Python-а ещё лучше. Например, в functools в последних версиях языка принесли функцию reduce, которая позволяет сжимать данные, применяя последовательно функцию и запоминая результат:

from functools import reduce

def multiply(a, b):
    return a * b

reduce(multiply, [1, 2, 3, 4, 5])
# reduce умножает 1 на 2, затем результат этого умножения на 3 и т.д.
120

То же самое при помощи анонимной функции:

reduce(lambda x, y: x * y, range(1, 6))
120

Генераторы

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

def even_range(start, end):
    current = start
    while current < end:
        yield current
        current += 2


for number in even_range(0, 10):
    print(number)
0
2
4
6
8

По генераторам нельзя делать слайсы, можно их выполять шаг за шагом.

ranger = even_range(0, 4)
next(ranger)
0

Списочные выражения

List comprehensions provide a concise way to create lists.

As list comprehension returns list, they consists of brackets containing the expression which needs to be executed for each element along with the for loop to iterate over each element.

zip

Ещё одна важная функция — функция zip — позволяет вам склеить два итерабельных объекта. В следующем примере мы по порядку соединяем объекты из num_list и squared_list в кортежи:

num_list = range(7)
squared_list = [x**2 for x in num_list]
list(zip(num_list, squared_list))
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25), (6, 36)]

Декораторы

Декоратор — это функция, которая принимает функцию и возвращает функцию.

def to_world(func):
    def new_f():
        return func() + " world"
    return new_f


@to_world
def helloer():
    return 'Hello'


helloer()
'Hello world'
hello_world = to_world(helloer)
hello_world()
'Hello world world'
print(helloer.__name__)
new_f

Пример: написать декоратор, который записывает в лог результат декорируемой функции. В этом примере с помощью декоратора logger мы подменяем декорируемую функцию функцией wrapped.

def logger(func):
    def wrapped(*args, **kwargs):
        result = func(*args, **kwargs)
        with open('log.txt', 'w') as f:
            f.write(str(result))
        return result

    return wrapped


@logger
def summator(num_list):
    return sum(num_list)

print('Summator: {}'.format(summator([1, 2, 3, 4])))
Summator: 10

Декоратору можно передавать параметры

def logger(filename):
    def decorator(func):
        def wrapped(*args, **kwargs): #   объяснить
            result = func(*args, **kwargs) # объяснить
            with open(filename, 'w') as f:
                f.write(str(result))
            return result
        return wrapped
    return decorator


@logger('new_log.txt')
def summator(num_list):
    return sum(num_list)

# без синтаксического сахара:
# summator = logger('new_log.txt')(summator)

Чтобы передавать данные между функциями, модулями или разными системами используются форматы данных. Одним из самых популярных форматов является JSON. Напишите декоратор to_json, который можно применить к различным функциям, чтобы преобразовывать их возвращаемое значение в JSON-формат (json.dumps).

@to_json
def get_data():
      return {
        'data': 42
      }
  
get_data()  # вернёт '{"data": 42}'
'42'

Области видимости функций

Схема разрешения имен в языке Python иногда называется правилом LEGB, название которого состоит из первых букв названий областей видимости.

Когда внутри функции выполняется обращение к неизвестному имени, интерпретатор пытается отыскать его в четырех областях видимости:

  • в локальной (local, L),
  • затем в локальной области любой объемлющей инструкции def (enclosing, E) или в выражении lambda,
  • затем в глобальной (global, G)
  • и, наконец, во встроенной (built-in, B).

Поиск завершается, как только будет найдено первое подходящее имя. Если требуемое имя не будет найдено, интерпретатор выведет сообщение об ошибке.

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

def extender(source_list, extend_list):
    source_list.extend(extend_list)


values = [1, 2, 3]
extender(values, [4, 5, 6])
print(values)
[1, 2, 3, 4, 5, 6]

Сделайте эту функцию «чистой».

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

from random import random

time = 5
car_positions = [1, 1, 1]

while time:
    # decrease time
    time -= 1

    print("")
    for i in range(len(car_positions)):
        # move car
        if random() > 0.3:
            car_positions[i] += 1

        # draw car
        print('-' * car_positions[i])

--
--
--

---
--
---

----
--
----

-----
---
-----

------
----
------

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

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

from random import random


def move_cars(car_positions):
    return list(map(lambda x: x + 1 if random() > 0.3 else x, car_positions))


def output_car(car_position):
    return '-' * car_position


def run_step_of_race(state):
    return {
        'time': state['time'] - 1,
        'car_positions': move_cars(state['car_positions'])
    }


def draw(state):
    print("")
    print('\n'.join(map(output_car, state['car_positions'])))


def race(state):
    draw(state)
    if state['time']:
        race(run_step_of_race(state))


race({'time': 5, 'car_positions': [1, 1, 1]})

-
-
-

--
--
--

---
---
--

---
----
--

----
----
---

-----
-----
----

Comments