Функциональное программирование на Python. Часть 2. Абстракция и композиция. Функции

Автор: | 11.02.2020

Если попытаться выделить наиболее фундаментальные концепции, которые используются в программировании, то это будут абстракция и композиция (Category: The Essence of Composition). Рассмотрим их через призму понятия функции с примерами на Python.

Для начал обратимся к википедии за определениями указанных выше терминов: 

Абстракция (лат. abstractio — отвлечение) — теоретическое обобщение как результат абстрагирования. Абстрагирование отвлечение в процессе познания от несущественных сторон, свойств, связей объекта (предмета или явления) с целью выделения их существенных, закономерных признаков. Результат абстрагирования — абстрактные понятия, например: цвет, кривизна, красота и т. д. 

Композиция (лат. compositio — составление, связывание, сложение, соединение) — составление целого из частей. 

Число

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

Начнем с базовой вещи – с понятия числа. То, как мы работаем с числами, это достаточно новое изобретение человечества. Для нас, когда мы говорим: пять яблок или пять стульев — это одни и те же пятерки. Число пять, в данном случае, это некоторая абстракция, которая позволяет не думать о содержании (стулья, яблоки и т.п.), а сосредоточиться на их количестве. Но понадобились тысячи лет эволюции, чтобы это произошло, чтобы мы (люди) могли отвлеченно использовать числа. Об этом интересно пишет Алексей Савватеев в книге “Математика для гуманитариев”, и приводит там такой пример: “… в русском языке до сих пор говорят “сорок” вместо “четырьдесят”, хотя раньше можно было сказать “сорок собольих шкурок”, но не “сорок деревьев”. То есть сорок означало не количество каких-либо предметов вообще, а только вполне определенных. Сейчас мы довольно свободно оперируем числами для счета предметов (и не только), а сами вычисления выполняем уже в отрыве от связи числа с сущностью. 

Функция

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

1+2=3 

4+7=11 

121+144=165 

… 

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

a + b = c 

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

>>> def add(a, b): 
        return a + b 

>>> add(1, 3) 
4 
>>> add(4, 7) 
11

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

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

Введем понятие lambda-функции (в программировании): lambda-функция – это безымянная функция с произвольным числом аргументов, вычисляющая одно выражение. В Python она определяется так: вначале записывается ключевое слово lambda, потом перечисляются через запятую аргументы, с которыми будет проводиться работа, далее ставится двоеточие, а после него тело функции. Более подробно про нее можете прочитать в статье Python. Урок 10. Функции в Python. 

Перейдем к решению задачи. Точку с координатами будем задавать как кортеж (см Python. Урок 8. Кортежи (tuple)). Напишем функцию, которая возводит в квадрат разность двух чисел: 

>>> sq_sub = lambda x1, x2: (x2 - x1)**2

Реализуем непосредственно саму функцию dist

>>> dist = lambda p1, p2: (sq_sub(p1[0], p2[0])+sq_sub(p1[1], p2[1]))**0.5

Проверим ее работу: 

>>> point1 = (1, 1) 
>>> point2 = (4, 5) 
>>> dist(point1, point2) 
5.0

Композиция функций

Исследуем задачу определения расстояния между точками с позиции идеи композиции из математики. Композицию можно выразить следующей диаграммой:

Математически она записывается вот так:

h = g ∘ f

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

Решим нашу задачу, задав все вычисления явно – через функции, для этого нам понадобятся: 

>>> sub = lambda a, b: a - b # функция вычитания 
>>> sq = lambda x: x**2      # функция возведения в квадрат 
>>> sm = lambda a, b: a + b  # функция сложения 
>>> sqrt = lambda x: x**0.5  # функция извлечения квадратного корня

Благодаря тому, что Python умеет распаковывать кортежи, мы может этим воспользоваться и реализовать dist следующим образом: 

>>> dist = lambda x1, y1, x2, y2: sqrt(sm(sq(sub(x1, x2)), sq(sub(y1, y2)))) 
>>> dist(*point1, *point2) 
5.0

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

Рекурсия

Используя приведенный выше подход создания функций и объединения их в композиции, можно решить довольно большое количество задач (и не только вычислительных): посчитать логарифм, вычислить синус или косинус заданного угла, определить квадрат числа и т.д. Все эти задачи объединяет то, что нам достаточно подставить какое-то число (или набор чисел), в качестве аргумента, в функцию, и мы получаем решение. Но бывают задачи, где этого недостаточно, например, определение факториала числа или элемента ряда Фибоначчи. Ключевое отличие этих задач заключается в том, что там присутствует некоторый вычислительный процесс, протекание которого определяется переданным аргументом. Другими словами, для того чтобы вычислить расстояния между точками по уже известной формуле, мы всегда будем делать одно и тоже количество операций: два вычитания, два возведения в степень, одно сложение и один раз извлечем корень. Количество умножений, которое придется выполнить, для того чтобы посчитать факториал, определяется числом, факториал которого мы хотим найти: для факториала пяти – пять умножений, для факториала десяти – десять.

Для построения такого типа (и не только) вычислительных процессов используется рекурсия. Суть рекурсии заключается в том, что функция вызывается из нее же самой. О рекурсии уже было достаточно много сказано на devpractice (https://devpractice.ru/fp-python-part1-general/#p32, https://devpractice.ru/about-rec-and-iter/), поэтому мы не будем на ней подробно останавливаться. 

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

Следующий шаг на пути к абстракции заключается в том, что если внимательно посмотреть на те программы, которые мы разрабатываем, то можно заметить, что у внешне, казалось бы, различных решений, будут находиться некоторые общие черты. Например, вам может понадобиться найти квадраты чисел заданного массива, или из данного набора чисел построить новый, элементами которого будут абсолютные значения элементов из исходного массива. Эти примеры наталкивают на идею построения некоторого внешнего интерфейса, принимающего в качестве аргументов набор данных и функцию, которая будет применяться к элементам этого набора. Идея, лежащая в этом типе абстракции та же, что использовалась нами, когда мы переходили от конкретных выражений 2+3=5, 7+8=15 и т.п. к самой операции сложения: мы абстрагировались от чисел, с которыми работали и сосредоточились на выполняемой операции. Так и здесь, мы можем абстрагироваться от частности: возведения всех элементов в квадрат или взятие абсолютного значения, и сосредоточиться на сути процесса: применение заданной функции к каждому элементу набора данных для построения нового набора. 

Так, мы приходим к идее, что нам нужны функции, которые могут использовать другие функции, переданные им в качестве аргументов. Такие конструкции носят название: функции высшего порядка (higher-order function). Так как функции, которые передаются в качестве аргументов, как правило, небольшие по размеру, то для удобства, их конструируют в виде lambd’ы прямо в месте вызова. Приведем несколько полезных свойств lambda-функций: 

Их можно вызвать в месте объявления: 

>>> (lambda x: x*2)(3)

Lambd’у можно сохранить в переменную и использовать ее в дальнейшем: 

>>> mul2 = lambda x: x*2 
>>> mul2(3) 
6

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

>>> list(map(lambda x: x**2, [1, 2, 3, 4, 5])) 
[1, 4, 9, 16, 25]

Функция list нужна для построения списка из результата работы map

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

>>> def is_config_valid(validator, data_struct): 
        return validator(data_struct)

Создадим новую конфигурацию: 

>>> tmp_conf = {"test1": 123}

Для нее подготовим валидатор: 

>>> is_tmp_valid = lambda x: True if "test1" in x else False

Проверим корректность конфигурации: 

>>> is_config_valid(is_tmp_valid, tmp_conf) 
True

В случае, если конфигурация имеет ошибки: 

>>> tmp2_conf = {"test2": 456}

Валидатор нам об этом сообщит: 

>>> is_config_valid(is_tmp_valid, tmp2_conf) 
False

Функция как возвращаемое значение. Каррирование, замыкание, частичное применение

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

Замыкание— функция, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся её параметрами. Поясним эту идею на примере: построим конструктор линейных функции вида y = k*x + b с заранее заданными коэффициентами: 

>>> def linear_builder(k, b): 
        def helper(x): 
            return k * x + b 
    return helper

Создадим линейную функцию со следующими параметрами 3 * x + 9 

>>> linf = linear_builder(3, 9) 
>>> linf(5) 
24

Если вас заинтересовала тема замыкания, рекомендуем вам обратиться к статье Замыкания в Python. 

 Каррирование — преобразование функции от многих аргументов в набор функций, каждая из которых является функцией от одного аргумента. Суть в том, чтобы перейти от вида f(x, y, z) к виду f(x)(y)(z). Каррирование позволяет строить частично примененные функции. 

Создадим функцию, которая складывает три числа: 

>>> def faddr(x, y, z): 
        return x + y + z 

>>> faddr(1, 2, 3) 
6

Каррированный вариант этой функции будет выглядеть так: 

>>> def curr_faddr(x): 
    def tmp_a(y): 
        def tmp_b(z): 
            return x+y+z 
        return tmp_b 
    return tmp_a 

>>> curr_faddr(1)(2)(3) 
6

На базе нее можно строить частично примененные функции: 

>>> p_c_faddr = curr_faddr(1)(2) 
>>> p_c_faddr(3) 
6

В этом примере p_c_faddr делает следующее 1+2+x, неизвестное значение x принимает в качестве аргумента. 

Функции – объекты первого класса 

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

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

Сущность, удовлетворяющая перечисленным выше требованиям, называется объектом первого класса (или объектом первого порядка). Как было отмечено в первой части, наличие таких свойств, является отличительной чертой функциональных языков программирования. В Python функции являются объектами первого класса, что позволяет придерживаться функционального стиля, когда это необходимо, но при этом создатель языка Гвидо ван Россум отмечает: “… хотя сделал функции полноправными объектами, никогда не рассматривал Python как язык функционального программирования”. 

P.S.

Вводные уроки по “Линейной алгебре на Python” вы можете найти соответствующей странице нашего сайта. Все уроки по этой теме собраны в книге “Линейная алгебра на Python”.
Книга: Линейная алгебра на Python
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas.  Для начала вы можете познакомиться с вводными уроками. Все уроки по библиотеке Pandas собраны в книге Pandas. Работа с данными”.
Книга: Pandas. Работа с данными

Поделиться
Share on VK
VK
Tweet about this on Twitter
Twitter
Share on Facebook
Facebook

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

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