Функциональное ядро на Python

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

Конвейер обработки данных
Конвейер обработки данных

Функциональный стиль программирования очень близок к тому, как размышляет человек во время решения задачи. «Пусть дано x. В целях решения задачи с этими данными необходимо выполнить серию преобразований. Сначала применить к ним f и получить результирующие данные x'. Затем к новым данным применить f2 и получить новые результирующие данные x'' и т.д.

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

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

    2                           

    |> ( fun x -> x + 5)        

    |> ( fun x -> x * x)        

    |> ( fun x -> x.ToString() )

Здесь входные данные, в данном случае число 2, последовательно обрабатываются серией лямбда-функций. Аналогичный конвейер можно имплементировать на языке Python, но для этого нужно написать специальную функцию, и, разумеется, это будет функция более высокого порядка:

# Конвейер обработки данных

 def pipe(data, *fseq):

    for fn in fseq:

        data = fn(data)

    return data

Приведенный ниже пример демонстрирует работу конвейера:

pipe(2,

     lambda x: x + 5,

     lambda x: x * x,

     lambda x: str(x))

Число 2 проходит серию преобразований, и в результате будет получено строковое значение '49'. По сравнению с функцией reduce, в которой переданная в качестве аргумента одна единственная редуцирующая функция по очереди применяется к последовательности данных, в функции pipe наоборот последовательность функций применяется к обновляемым данным.

Функция pipe получает два аргумента: входные данные data и последовательность функций fseq. Во время первой итерации цикла for данные передаются в первую функцию из последовательности. Эта функция обрабатывает данные и возвращает результат, замещая переменную data новыми данными. Затем эти новые данные отправляются во вторую функцию и т.д. до тех пор, пока не будут выполнены все функции последовательности. По завершению своей работы функция pipe возвращает итоговые данные. Это и есть конвейер обработки данных.

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

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

def my_sum(*args): # Упаковка в список

    return sum(args)

 

my_sum(1, 2, 3, 4, 5)

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

def fun(a, b, c, d):

    print(a, b, c, d)

 

my_list = [1, 2, 3, 4]

fun(*my_list) # Разложение на четыре аргумента

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

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

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

1 # Эта программа демонстрирует

 2 # функциональную версию функции factorial из главы 12

 3

 4 def main():

 5     # Конвейер (ядро c нерекурсивным алгоритмом факториала)

 6     pipe(int(input('Введите неотрицательное целое число: ')),   

 7          lambda n: (n, reduce(lambda x, y: x * y, range(1, n + 1))),   

 8          lambda tup:

 9              print(f'Факториал числа {tup[0]} равняется {tup[1]}'))       

 

# Вызвать главную функцию

main()

Вывод программы:

Введите неотрицательное целое число: 4 (Enter)

Факториал числа 4 равняется 24

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

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

1 # Эта программа демонстрирует

 2 # функциональную версию функции factorial из главы 12

 3

 4 def get_int(msg=''):

 5     return int(input(msg))

 6

 7 def main():

 8     # Алгоритм 1. Рекурсивная версия с хвостовой рекурсией

 9     def factorial_rec(n):

10         fn = lambda n, acc=1: acc if n == 0 else fn(n - 1, acc * n)

11         return n, fn(n)

12   

13     # Алгоритм 2. Нерекурсивная версия

14     def factorial(n):    

15         return n, reduce(lambda x, y: x * y, range(1, n + 1))

16   

17     # Ввод данных

18     def indata():

19         def validate(n):  # Валидация входных данных

20             if not isinstance(n, int):

21                 raise TypeError("Число должно быть целым.")

22             if not n >= 0:

23                 raise ValueError("Число должно быть >= 0.")

24             return n       

25         msg = 'Введите неотрицательное целое число: '

26         return pipe(get_int(msg), validate)

27   

28     # Вывод данных

29     def outdata():

30         def fn(data):

31             n, fact = data

32             print(f'Факториал числа {n} равняется {fact}')

33         return fn

34

35     # Конвейер (функциональное ядро)

36     pipe(indata(),     # вход: -       выход: int

37          factorial,    # вход: int     выход: кортеж

38          outdata())    # вход: кортеж  выход: -   

39

40 # Вызвать главную функцию

41 main()

Вывод программы:

Введите неотрицательное целое число: 4 (Enter)

Факториал числа 4 равняется 24

Функциональным ядром программы являются строки 36-38:

pipe(indata(),

     factorial,

     outdata())

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

Следует отметить два важных момента. Во-первых, передаваемые от узла к узлу данные должны соответствовать какому-то определенному протоколу. Во-вторых, количество узлов может быть любым.

Такая организация программного кода:

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

pipe(indata(), factorial_rec, outdata())

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

Например, рассмотрим вторую возможность – отладку. В этом случае можно написать вспомогательную функцию check:

def check(data):

    print(data)

    return data

И затем ее вставить в конвейер, чтобы проверить результаты работы отдельных узлов конвейера:

pipe(indata(), check, factorial, check, outdata())

Если выполнить программу в таком варианте, то будут получены следующие результаты:

Вывод программы:

Введите неотрицательное целое число: 4 (Enter)

4

(4, 24)

Факториал числа 4 равняется 24

Как видно из результатов, на вход в функцию factorial поступает введенное пользователем значение 4, а на выходе из нее возвращается кортеж с исходным числом и полученным результатом (4, 24). Этот результат показывает, что программа работает, как и ожидалось. Как вариант, вместо проверочной функции можно написать функцию-таймер, которая могла бы хронометрировать отдельные узлы конвейера.

Приведем еще пару примеров с аналогичной организацией программного кода на основе функционального ядра в виде конвейера.

Функциональная имплементация вычисления последовательности Фибоначчи

# Эта программа демонстрирует

# функциональную версию функции fibonacci из главы 12

 

def main():

    # Алгоритм

    def fibonacci(n, x=0, y=1):

        # Функция fib возвращает n-ое число последовательности.

        fib = lambda n, x=0, y=1: x if n <= 0 else fib(n - 1, y, x + y)

        # Функция reduce собирает результаты в список acc

        acc = []

        reduce(lambda _, y: acc.append(fib(y)), range(n + 1))

        return n, acc

   

    # Валидация входных данных

    def validate(n):        

        if not isinstance(n, int):

            raise TypeError("Число должно быть целым.")

        if not n >= 0:

            raise ValueError("Число должно быть ноль положительным.")

        if n > 10:

            raise ValueError("Число должно быть не больше 10.")

        return n

   

    # Ввод данных

    def indata():

        msg = 'Введите неотрицательное целое число не больше 10: '

        return pipe(get_int(msg), validate)

   

    # Вывод данных

    def outdata():

        def fn(data):

            n, seq = data

            msg = f'Первые {n} чисел последовательности Фибоначчи:'

            print(msg)

            [print(el) for el in seq]

        return fn

 

    # Конвейер (функциональное ядро)

    pipe(indata(), fibonacci, outdata())

 

# Вызвать главную функцию.

main()

Вывод программы

Введите неотрицательное целое число не больше 10: 10 (Enter)

Первые 10 чисел последовательности Фибоначчи:

1

1

2

3

5

8

13

21

34

55

Функциональная имплементация суммирования диапазона значений последовательности

# Эта программа демонстрирует

# функциональную версию функции range_sum из главы 12

 

def main():

    # Алгоритм

    def range_sum(data): 

        seq, params = data

        fn = lambda start, end: 0 if start > end \

                                  else seq[start] + fn(start + 1, end)

        return fn(*params)

 

    # Ввод данных

    def indata():

        seq = [1, 2, 3, 4, 5, 6, 7, 8, 9]   

        params = (2,5)   # params - это параметры start, end

        return seq, params 

   

    # Вывод данных

    def outdata():

        def f(data):

            msg = 'Сумма значений со 2 по 5 позиции равняется '

            print(msg, format(data), sep='')

        return f

   

    # Конвейер (функциональное ядро)

    pipe(indata(), range_sum, outdata())

 

# Вызвать главную функцию.

main()

Вывод программы

Сумма значений со 2 по 5 позиции равняется 18

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

ссылка на оригинал статьи https://habr.com/ru/post/555370/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *