Рубрики
Без рубрики

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

Автор оригинала: Marcus Sanatan.

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

Вступление

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

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

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

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

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

  • Чистые функции – не имеют побочных эффектов, то есть не изменяют состояние программы. При одном и том же входе чистая функция всегда будет производить один и тот же выход.
  • Неизменяемость – данные не могут быть изменены после их создания. Возьмем, к примеру, создание списка с 3 элементами и хранение его в переменной my_list . Если my_list является неизменяемым, вы не сможете изменить отдельные элементы. Вам придется установить my_list в новый List , если вы хотите использовать другие значения.
  • Функции более высокого порядка – функции могут принимать другие функции в качестве параметров, а функции могут возвращать новые функции в качестве выходных данных. Это позволяет нам абстрагироваться от действий, давая нам гибкость в поведении вашего кода.

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

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

Без каких-либо специальных функций Python или библиотек мы можем начать кодирование более функциональным способом.

Чистые Функции

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

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

Давайте создадим чистую функцию для умножения чисел на 2:

def multiply_2_pure(numbers):
    new_numbers = []
    for n in numbers:
        new_numbers.append(n * 2)
    return new_numbers

original_numbers = [1, 3, 5, 10]
changed_numbers = multiply_2_pure(original_numbers)
print(original_numbers) # [1, 3, 5, 10]
print(changed_numbers)  # [2, 6, 10, 20]

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

Неизменность

Когда-нибудь была ошибка, когда вы задавались вопросом, как переменная, которую вы установили в 25, стала None ? Если бы эта переменная была неизменной, ошибка была бы вызвана там, где переменная изменялась, а не там, где измененное значение уже повлияло на программное обеспечение – первопричину ошибки можно найти раньше.

Python предлагает несколько неизменяемых типов данных, популярным из которых является Кортеж . Давайте сравним кортеж со списком , который является изменяемым:

mutable_collection = ['Tim', 10, [4, 5]]
immutable_collection = ('Tim', 10, [4, 5])

# Reading from data types are essentially the same:
print(mutable_collection[2])    # [4, 5]
print(immutable_collection[2])  # [4, 5]

# Let's change the 2nd value from 10 to 15
mutable_collection[1] = 15

# This fails with the tuple
immutable_collection[1] = 15

Ошибка, которую вы увидите: TypeError: Объект 'tuple' не поддерживает назначение элемента .

Теперь есть интересный сценарий, в котором Кортеж может оказаться изменяемым объектом. Например, если бы мы хотели изменить список в immutable_collection с [4, 5] на [4, 5, 6] , вы можете сделать следующее:

immutable_collection[2].append(6)
print(immutable_collection[2])  # [4, 5, 6]

Это работает, потому что List является изменяемым объектом. Давайте попробуем изменить список обратно на [4, 5] .

immutable_collection[2] = [4, 5]
# This throws a familiar error:
# TypeError: 'tuple' object does not support item assignment

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

Функции Более высокого Порядка

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

Рассмотрим функцию, которая печатает строку несколько раз:

def write_repeat(message, n):
    for i in range(n):
        print(message)

write_repeat('Hello', 5)

Что делать, если мы хотим записать в файл 5 раз или записать сообщение в журнал 5 раз? Вместо того чтобы писать 3 различные функции, которые все циклически повторяются, мы можем написать 1 функцию Более высокого порядка, которая принимает эти функции в качестве аргумента:

def hof_write_repeat(message, n, action):
    for i in range(n):
        action(message)

hof_write_repeat('Hello', 5, print)

# Import the logging library
import logging
# Log the output as an error instead
hof_write_repeat('Hello', 5, logging.error)

Теперь представьте, что перед нами стоит задача создать функции, которые увеличивают числа в списке на 2, 5 и 10. Начнем с первого случая:

def add2(numbers):
    new_numbers = []
    for n in numbers:
        new_numbers.append(n + 2)
    return new_numbers

print(add2([23, 88])) # [25, 90]

Хотя тривиально писать функции add 5 и add 10 , очевидно, что они будут работать одинаково: перебирать список и добавлять инкремент. Поэтому вместо того, чтобы создавать множество различных функций приращения, мы создаем 1 функцию более высокого порядка:

def hof_add(increment):
    # Create a function that loops and adds the increment
    def add_increment(numbers):
        new_numbers = []
        for n in numbers:
            new_numbers.append(n + increment)
        return new_numbers
    # We return the function as we do any other value
    return add_increment

add5 = hof_add(5)
print(add5([23, 88]))   # [28, 93]
add10 = hof_add(10)
print(add10([23, 88]))  # [33, 98]

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

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

Лямбда-выражения

Лямбда – выражение-это анонимная функция. Когда мы создаем функции в Python, мы используем ключевое слово def и даем ему имя. Лямбда-выражения позволяют определить функцию гораздо быстрее.

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

def hof_product(multiplier):
    return lambda x: x * multiplier

mult6 = hof_product(6)
print(mult6(6)) # 36

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

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

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

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

Карта

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

names = ['Shivani', 'Jason', 'Yusef', 'Sakura']
greeted_names = map(lambda x: 'Hi ' + x, names)

# This prints something similar to: 
print(greeted_names)
# Recall, that map returns an iterator 

# We can print all names in a for loop
for name in greeted_names:
    print(name)

Фильтр

Функция filter проверяет каждый элемент в итерационном объекте с помощью функции , которая возвращает либо True , либо False , сохраняя только те, которые оцениваются как Правда . Если бы у нас был список чисел и мы хотели бы сохранить те из них, которые делятся на 5, мы могли бы сделать следующее:

numbers = [13, 4, 18, 35]
div_by_5 = filter(lambda num: num % 5 == 0, numbers)

# We can convert the iterator into a list
print(list(div_by_5)) # [35]

Комбинирование карты и фильтра

Поскольку каждая функция возвращает итератор, и они оба принимают итеративные объекты, мы можем использовать их вместе для некоторых действительно выразительных манипуляций с данными!

# Let's arbitrarily get the all numbers divisible by 3 between 1 and 20 and cube them
arbitrary_numbers = map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21)))

print(list(arbitrary_numbers)) # [27, 216, 729, 1728, 3375, 5832]

Выражение в arbitrary_numbers можно разбить на 3 части:

  • range(1, 21) – это итеративный объект, представляющий числа из 1, 2, 3, 4… 19, 20.
  • фильтр(лямбда-число: число % 3, диапазон(1, 21)) является итератором для числовой последовательности 3, 6, 9, 12, 15 и 18.
  • Когда они кубируются выражением map , мы можем получить итератор для числовой последовательности 27, 216, 729, 1728, 3375 и 5832.

Список Постижений

Популярная функция Python, которая заметно проявляется в функциональных языках программирования, – это понимание списков. Как и функции map и filter , понимание списка позволяет нам изменять данные кратким и выразительным способом.

Давайте попробуем наши предыдущие примеры с map и filter с пониманием списка вместо этого:

# Recall
names = ['Shivani', 'Jan', 'Yusef', 'Sakura']
# Instead of: map(lambda x: 'Hi ' + x, names), we can do
greeted_names = ['Hi ' + name for name in names]

print(greeted_names) # ['Hi Shivani', 'Hi Jason', 'Hi Yusef', 'Hi Sakura']

Базовый список пониманий следует этому формату: [result for singular-element in list-name].

Если мы хотим фильтровать объекты, то нам нужно использовать ключевое слово if :

# Recall
numbers = [13, 4, 18, 35]
# Instead of: filter(lambda num: num % 5 == 0, numbers), we can do
div_by_5 = [num for num in numbers if num % 5 == 0]

print(div_by_5) # [35]

# We can manage the combined case as well:
# Instead of: 
# map(lambda num: num ** 3, filter(lambda num: num % 3 == 0, range(1, 21)))
arbitrary_numbers = [num ** 3 for num in range(1, 21) if num % 3 == 0]
print(arbitrary_numbers) # [27, 216, 729, 1728, 3375, 5832]

Каждое выражение map и filter может быть выражено как понимание списка.

Некоторые вещи, которые следует учитывать

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

Кроме того, сообщество разработчиков Python не поощряет использование широкого спектра функциональных возможностей программирования. Если бы вы писали код для глобального сообщества Python, вы бы написали список постижений вместо использования map или filter . Лямбды будут использоваться минимально, так как вы будете называть свои функции.

В вашем интерпретаторе Python введите import this , и вы увидите “Дзен Python”. Python обычно поощряет написание кода наиболее очевидным способом. В идеале весь код должен быть написан одним способом – сообщество не считает, что он должен быть в функциональном стиле.

Вывод

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

Python позволяет нам кодировать в функциональном, декларативном стиле. Он даже поддерживает многие общие функциональные функции, такие как лямбда-выражения и функции map и filter .

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