Статьи

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

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

Что такое функциональное программирование?

Цитировать из Википедии,

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

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

Поскольку подробное обсуждение функционального программирования (далее упоминаемого как FP) выходит за рамки этого поста, я просто кратко изложу наиболее важные элементы FP.

  • Функции как основные строительные блоки: неудивительно, что FP требует создания и использования функций в качестве основных строительных блоков. В самых простых терминах « int add (int x, int y) {return x + y; } »- простая функция сложения, написанная на« C ». Он принимает два параметра x и y, добавляет их и возвращает результат. Это довольно очевидный и простой случай, но я изложил его, так как я хотел бы вернуться к нему позже в этом посте.
  • Функциональное программирование предпочитает функцию без побочных эффектов: добавить выше функцией является хорошим примером функции без побочных эффектов. Считается, что функция не имеет побочных эффектов, если единственные изменения, которые она вносит, это те, которые проявляются в возвращаемых значениях. Другими словами, такая функция не может изменять какие-либо глобальные переменные, записывать в консоль, обновлять базу данных и т. Д. Достаточно связанный термин — ссылочная прозрачность . Функция называется прозрачной по ссылкам, если ее вызов может быть заменен возвращаемым значением в программе без какого-либо влияния на программу.
  • Неизменность . Чистое функциональное программирование часто требует от вас работы с неизменяемыми структурами данных. Таким образом, значение любой переменной не подлежит изменению (поэтому они называются значениями, а не переменными). Этот аспект дополняет функции без побочных эффектов. Таким образом, большинство изменений состояния реализуются не путем изменения объекта на месте (как это требует императивное программирование), а путем клонирования структуры данных с изменением некоторых значений и измененной структуры данных, возвращаемой функцией. , Java-программисты знают об неизменности экземпляров String, когда любые изменения строки приводят к созданию нового экземпляра String. Представьте, что то же самое происходит со всеми типами данных в программе.

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

Некоторые из приятных преимуществ (я испытываю желание сказать побочные эффекты) функционального программирования:

  • Превосходная способность работать с параллелизмом (многопоточность): Резьбовые программы писать неприятно. И противнее отлаживать. В императивной среде вам приходится иметь дело не только со структурами данных, изменяемыми на месте некоторыми другими частями программы, в многопоточной среде такие изменения могут происходить с использованием одноранговых потоков, даже если ваш текущий поток, на логике которого вы сосредоточены, пытаясь использовать эту логику. Это чрезвычайно непредсказуемая среда, которая привела к ряду практических рекомендаций по безопасному многопоточному программированию с использованием таких конструкций, как блокировки, мьютексы и т. Д. Функциональное программирование решает эту проблему гораздо более элегантно. Вместо того чтобы контролировать непредсказуемость и управлять ею, она полностью ее устраняет.Поскольку однажды созданная структура данных не будет изменена, а источник изменений может быть четко расположен для функции, которая создала структуру данных, непредсказуемость изменения данных прямо под вами исчезла. Это может быть немного дорого, и FP иногда сталкивается с некоторыми компромиссами (или интересными функциями в зависимости от того, как вы это видите), такими как программная транзакционная память, но обсуждение этого вопроса полностью выходит за рамки этого поста.
  • Более легкое тестирование и отладка: Поскольку изменения в данных содержатся, а функция взаимодействует с внешним контекстом только через свои возвращаемые значения, тестирование и отладка становятся намного проще. По сути, вам нужно сосредоточиться на тестировании каждой функции в отдельности. Точно так же во время отладки вы должны быть в состоянии быстро найти функцию, которая может иметь проблему, после чего вы можете легко сосредоточиться на функции, чтобы иметь возможность быстро решить проблему. Выделение функций также может помочь в тестировании каждой функции по отдельности. В целом, из-за меньшего количества побочных эффектов тестирование в рамках функционального программирования часто намного проще, и важность проведения «интеграционного» и «модульного» тестирования меньше, поскольку тестирование функций в отдельности, вероятно, выявляет большинство проблем, гораздо больше. чем в типичном императивном программировании.

Почему питон?

Python не самый лучший функциональный язык программирования. Но это не должно было быть. Python — это мультипарадигмальный язык. Хотите написать старый добрый процедурный код в стиле C? Python сделает это за вас. Объектный код в стиле C ++ / Java? Python также к вашим услугам. Функциональное программирование? Поскольку эта серия постов собирается продемонстрировать — Python также может неплохо с этим справиться. Python, вероятно, является наиболее продуктивным языком, с которым я работал (для самых разных требований программирования). Добавьте к этому тот факт, что python является языком, который чрезвычайно прост в изучении, обладает отличной читабельностью, имеет довольно хорошие веб-фреймворки, такие как django , имеет отличные математические и статистические библиотеки, такие как numpy , и классные сетевые ориентированные фреймворки, такие каквитая . Python не может быть правильным выбором, если вы хотите написать 100% FP. Но если вы хотите узнать больше о FP или использовать технику FP вместе с другими парадигмами , возможности Python будут кричать, чтобы их услышали .

Пример программы:

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

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

Неизменные данные:

from collections import namedtuple

Context = namedtuple('Context','stack, current, op')
def default_context():
    return Context([],0.0,None)

Python не особенно силен в неизменных данных. Однако одна из структур данных, кортеж является неизменной. Namedtuple это другая структура данных , которая поддерживает как кортеж , как доступ через индексы или через названные элементы в кортеже. Для калькулятора мне понадобится Context, который содержит стек для хранения любых незавершенных операций, атрибут current, отражающий текущее значение, отображаемое на экране, и опциюкоторая может отражать ожидающую операцию, которая обычно требуется для бинарных операторов, где все еще необходимо предоставить второе значение. Хотя namedtuple является разумной конструкцией для простых объектов, подобных кортежу, было бы полезно иметь также неизменяемые объекты, но об этом будет сказано в следующем посте.

Простые функции

def add(x,y): return x + y
def sub(x,y): return x - y
def mult(x,y): return x * y
def div(x,y): return x/ y
def reverse_sign(x): return -1 * x
def pow(x,y): return x ** y

Там не так много, чтобы описать здесь. Функции должны быть самоочевидными. В целях акцентирования я хотел бы отметить, что в приведенном выше коде «add» теперь является записью в пространстве имен, которая является ссылкой на функцию. Эту ссылку можно обойти, присвоить другим записям. Таким образом, приведенный ниже код должен работать (хотя он не является частью программы калькулятора). Это демонстрирует, как python обрабатывает атрибуты и функции, практически идентичные принципу единого доступа .

otheradd = add
add = sub
assert otheradd(7,3) == 10
assert add(7,3) == 4

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

from functools import partial

square = partial(pow,y=2)

Здесь частичное является ссылкой на функцию. квадрат теперь относится к другой функции с привязкой значения параметра y к 2

Динамический вызов функций

unary_functions = {'!' : reverse_sign, '@' : square }

def handle_unary_op(ctx,x):
    return ctx._replace(current = unary_functions[x](ctx.current), op = None)

binary_functions = {'+' : add, '-' : sub, '*' : mult, '/' : div}

def handle_binary_op(ctx,x):
    return ctx._replace(op = binary_functions[x])

def handle_float(ctx,x):
    if not ctx.op :
        return ctx._replace(current = x)
    else :
        return ctx._replace(current = ctx.op(ctx.current,x), op = None)

Обратите внимание, что я создал файл unary_functions dict (или словарь или hashmap), где ключ — это символ, представляющий функцию, а значение — ссылка на функцию.

Также обратите внимание, что в функции handle_unary_op я вызываю метод ctx._replace . В именованном кортеже он создает другой кортеж на основе существующих данных именованного кортежа, но с некоторыми значениями, измененными, как указано в параметрах ключевого слова, передаваемых _replace . После поиска соответствующей унарной функции т.е. unary_functions [x] , я также вызываю его для текущего значения, т.е. unary_functions [x] (ctx.current) . Я также определил другой диктат для бинарных операторов. Метод handle_binary_op отражает, как в контексте задается соответствующая двоичная функция, которая должна запускаться после того, как известно следующее значение.

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

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

def start_brace(ctx):
    newstack = ctx.stack
    newstack.append((ctx.current,ctx.op))
    return ctx._replace(
                stack = newstack,
                current = 0.0, op = None)

def end_brace(ctx):
    stack = ctx.stack
    current = ctx.current
    oldcurrent, oldop = stack.pop()
   
    oldctx = Context(stack,oldcurrent,oldop)
    return process_key(oldctx,current)

tokens = { '(': start_brace, ')' : end_brace}

def handle_tokens(ctx,x):
    return tokens[x](ctx)

Обрабатывая один ключ,
я должен признаться, что я начал с использования ключа для представления нажатий клавиш, но по ходу действия ключ также может представлять полное число с плавающей запятой (а не только одно нажатие клавиши). Таким образом, ключевой параметр может ссылаться на односимвольный оператор или последовательность символов, представляющих число с плавающей запятой

function_groups = {
    tuple(unary_functions.keys()) : handle_unary_op,
    tuple(binary_functions.keys()) : handle_binary_op,
    tuple(tokens.keys()) : handle_tokens
}

def process_key(ctx,key):
    if isinstance(key,(types.FloatType,types.IntType, types.LongType)) :
        return handle_float(ctx,key)
    elif isinstance(key,(types.StringType)) :
        for function_class in function_groups :
            if key in function_class : return function_groups[function_class](ctx,key)
    return ctx

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

Функция process_key принимает входящий ключ, передает его handle_float, если это число, или обрабатывает его как оператор. Если это последний, он ищет его во всех ключах каждой группы операторов, и если он находит совпадение, он находит соответствующую функцию-обработчик на карте и вызывает ее. Наконец, если совпадение не найдено, ключ игнорируется.

Обработка последовательности клавиш

def process_keys(keys):
    return reduce(lambda ctx,key : process_key(ctx,key), keys, default_context())

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

Чтобы интерпретировать использование, прочитайте приведенный выше оператор сокращения справа налево. Таким образом, мы начинаем с контекста по умолчанию, и для каждого ключа в последовательности ключей мы вызываем лямбду (то есть анонимную функцию), которая вызывает ключ процесса с контекстом и ключом. Обратите внимание, что первый параметр лямбды — это либо начальное значение (контекст по умолчанию), либо возвращаемое значение последнего process_key (который также является контекстом), а ключ — это каждый ключ в последовательности ключей, вводимой последовательно.

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

def process_keys(keys):
    ctx = default_context()
    for key in keys :
        ctx = process_key(ctx,key)
    return ctx

Использование
В качестве механизма для проведения некоторых элементарных тестов кода, написанного до сих пор, представлен следующий код. Здесь вы можете получить общее представление о программе.

if __name__ == "__main__" :
    assert process_keys((2,'+', 3)).current == 5
    assert process_keys((2, '!', '+', 5)).current == 3
    assert process_keys((2, '@')).current == 4
    assert process_keys((2,'+',3,'*',5)).current == 25
    assert process_keys((2,'+','(',3,'*',5,')')).current == 17

Еще немного более продвинутое функциональное программирование

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

from itertools import chain

def plus_num_seq(n):
    count = 1
    while count <= n :
        yield '+', count
        count += 1

keys = list(chain.from_iterable(plus_num_seq(10)))[1:]
assert process_keys(keys) == 55

Plus_num_seq представляет собой генератор. Обратите внимание на использование оператора yield. Таким образом, он будет непрерывно генерировать кортежи, где первый элемент кортежа будет символом «+», а второй — числом с числом, варьирующимся от значений от 1 до n. В chain.from_iterable сглаживает генерируется список (это , таким образом , имеет 20 элементов для п = 10, каждый из которых чередуются один является «+» символов , начиная с первых пунктов). Поскольку нам не нужен самый первый символ «+», я удалил его, используя оператор среза [1:].

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

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

Примечание. Полный исходный код калькулятора calculator.py доступен здесь .

Обновление: я намного позже также провел презентацию на ту же тему на Pycon India 2010. Слайды к теме можно найти внизу этой страницы (прямая ссылка: talk.html )

Источник:  http://blog.dhananjaynene.com/2010/02/functional-programming-with-python-part-1