FP в Python: Является ли он истинно функциональным?

Если вы следите за мной в Twitter, то, возможно, заметили, что я написал что-то о том, что FP и ООП в Python — это одно и то же. Позже я понял, что большинство Pythonistas не поняли идею, которую я пытался донести. Поэтому я решил написать статью, чтобы дать вам полную сводку моих недавних открытий в Python.

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

👉 Оглавление (TOC).

  • Введение.
  • Что такое объектно-ориентированное программирование?
  • Что такое функциональное программирование?
  • Заключение.
  • Ссылки и ресурсы.

Введение.

🔝 Go To TOC.

Объектно-ориентированное программирование и функциональное программирование — это не одно и то же, это две разные парадигмы со своими преимуществами и недостатками. Однако в python разделение между этими двумя парадигмами было размыто, поскольку все моделируется как объект, хранящийся в куче [0]. На самом деле оказалось, что все, что можно сделать с классами, например, наследование и композицию, в python можно реализовать с помощью функций: они же вызываемые объекты.

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

Что такое объектно-ориентированное программирование?

🔝 Go To TOC.

Объектно-ориентированное программирование — это парадигма программирования, которая представляет концепцию объектов как структур данных, содержащих данные, они же состояния, и методы, они же поведение. Это способ разработки программ путем разбиения их на более мелкие части, называемые объектами, которые могут быть использованы для моделирования реальных сущностей, таких как животные, автомобили и т.д. Объектно-ориентированный подход к программированию существует с 1960-х годов, и он был популяризирован такими языками, как Java, C++, Python, Ruby и др.

Поскольку эта статья связана только с языком python, давайте возьмем следующий листинг для моделирования животного с поведением. Для простоты предположим, что у животного есть только два основных свойства: имя и цвет. Кроме того, мы можем использовать пакет attrs, чтобы избежать шаблонного кода (например, __init__) в нашем классе.

from attrs import define

@define
class Animal:
  name: str
  color: str
  def run(self) -> None:
    print(f"An animal whose name: {self.name} and color: {self.color} is currently running!")
Вход в полноэкранный режим Выход из полноэкранного режима

В python 2 определение этого класса было бы немного другим: class Animal(object). Поскольку мы используем python 3, каждый класс по умолчанию наследует от класса object. Более того, объекты можно создавать, манипулировать ими и уничтожать. Доступ к данным объекта можно получить, используя имя объекта, ссылочную переменную, за которой следует оператор «точка», называемый «точечной нотацией». То же самое относится и к методам объекта.

animal = Animal("An animal", "purple")
print(animal.name)
animal.run()
animal.color = "blue"
del animal
Вход в полноэкранный режим Выход из полноэкранного режима

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

An animal
An animal whose name: An animal and color: purple is currently running!
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

from attrs import define

@define
class Dog(Animal):
  def run(self) -> None:
    print(f"A dog whose name: {self.name} and color: {self.color} is currently running!")
Вход в полноэкранный режим Выход из полноэкранного режима

Как отмечалось ранее, объект можно создавать, манипулировать им и уничтожать. Доступ к состояниям и поведению объекта можно получить с помощью точечной нотации.

dog = Dog("Finn", "white")
dog.run()
dog.color = "blue"
del dog
Вход в полноэкранный режим Выход из полноэкранного режима

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

A dog whose name: Finn and color: white is currently running!
Вход в полноэкранный режим Выход из полноэкранного режима

Можно даже сделать многоуровневое наследование.

from attrs import define

@define
class Puppy(Dog):
  def run(self) -> None:
    print(f"A puppy whose name: {self.name} and color: {self.color} is currently running!")
Войти в полноэкранный режим Выход из полноэкранного режима

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

from attrs import define

@define
class Puppy(Animal, object):
  def run(self) -> None:
    print(f"A puppy whose name: {self.name} and color: {self.color} is currently running!")
Войти в полноэкранный режим Выход из полноэкранного режима

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

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

🔝 Go To TOC.

Функциональное программирование — это парадигма программирования, которая рассматривает вычисления как оценку математических функций и избегает побочных эффектов и изменяемых данных. Это означает, что данная парадигма не использует переменные для хранения информации. Вместо этого она использует неизменяемые значения, которые не могут быть изменены после их создания. Это декларативная парадигма, что означает, что блок-схема программы состоит из таких утверждений, как f(x) = a.x + b.

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

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

def animal(name: str, color: str) -> None:
  def run():
    print(f"An animal whose name: {name} and color: {color} is currently running!")
  return run
Вход в полноэкранный режим Выйти из полноэкранного режима

До сих пор все выглядит как обычно. Но у нашего вызываемого объекта animal отсутствуют атрибуты, в связи с чем возникает вопрос: Можно ли это сделать в python? И ответ — да, начиная с pep-232 [1].

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal():
  ...
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем определить атрибуты и методы для вызываемого объекта animal следующим образом:

animal.name = "An animal"
animal.color = "blue"
animal.run = run
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы можем вызвать внутренний вызываемый объект, как показано в листинге ниже:

animal.run()
Войти в полноэкранный режим Выйти из полноэкранного режима

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

An animal whose name: An animal and color: blue is currently running!
Вход в полноэкранный режим Выход из полноэкранного режима

Мы можем даже изменить внутренние атрибуты, как показано ниже:

animal.name = "Another animal"
animal.run()
del animal
Войти в полноэкранный режим Выйти из полноэкранного режима

Что приводит к:

An animal whose name: Another animal and color: blue is currently running!
Ввести полноэкранный режим Выйти из полноэкранного режима

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

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal(func):
  def inner(*args, **kwargs):
    return func(*args, **kwargs)
  return inner

animal.name = "An animal"
animal.color = "blue"
animal.run = run

@animal
def dog():
  ...
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем определить атрибуты и методы для вызываемого объекта dog следующим образом:

def run():
  global dog
  print(f"A dog whose name: {dog.name} and color: {dog.color} is currently running!")

dog.name = "Finn"
dog.color = "blue"
dog.run = run
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы можем вызвать внутренний вызываемый объект, как показано в листинге ниже:

dog.run()
Войти в полноэкранный режим Выйти из полноэкранного режима

Выход:

A dog whose name: Finn and color: blue is currently running!
Ввести полноэкранный режим Выйти из полноэкранного режима

Мы можем даже изменить внутренние атрибуты, как показано ниже:

dog.name = "Katie"
dog.run()
del dog
Ввести полноэкранный режим Выход из полноэкранного режима

Выход:

A dog whose name: Katie and color: blue is currently running!
Ввести полноэкранный режим Выйти из полноэкранного режима

Однако эффект декоратора полностью уничтожен, потому что мы мало что сделали во внутренней функции. Чтобы проиллюстрировать механизм работы внутренней функции, мы можем вызвать функцию run вызываемого объекта animal.

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal(func):
  def inner(*args, **kwargs):
    animal.run()
    return func(*args, **kwargs)
  return inner

animal.name = "An animal"
animal.color = "blue"
animal.run = run

@animal
def dog() -> None:
  ...
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, инстанцируя объект dog, мы выполним функцию run вызываемого объекта animal:

dog()
Войти в полноэкранный режим Выйти из полноэкранного режима

Выход:

An animal whose name: An animal and color: blue is currently running!
Войти в полноэкранный режим Выход из полноэкранного режима

Вы даже можете сделать многоуровневое наследование, как показано ниже:

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal(func):
  def inner(*args, **kwargs):
    animal.run()
    return func(*args, **kwargs)
  return inner

animal.name = "An animal"
animal.color = "blue"
animal.run = run

def run():
  global dog
  print(f"A dog whose name: {dog.name} and color: {dog.color} is currently running!")

@animal
def dog(func):
  def inner(*args, **kwargs):
    dog.run()
    return func(*args, **kwargs)
  return inner

dog.name = "Finn"
dog.color = "blue"
dog.run = run

@dog
def puppy() -> None:
  ...

puppy()
Вход в полноэкранный режим Выход из полноэкранного режима

Выход:

An animal whose name: An animal and color: blue is currently running!
A dog whose name: Finn and color: blue is currently running!
Вход в полноэкранный режим Выход из полноэкранного режима

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

def run() -> None:
  global animal
  print(f"An animal whose name: {animal.name} and color: {animal.color} is currently running!")

def animal(func):
  def inner(*args, **kwargs):
    animal.run()
    return func(*args, **kwargs)
  return inner

animal.name = "An animal"
animal.color = "blue"
animal.run = run

def run() -> None:
  global dog
  print(f"A dog whose name: {dog.name} and color: {dog.color} is currently running!")

def dog(func):
  def inner(*args, **kwargs):
    dog.run()
    return func(*args, **kwargs)
  return inner

dog.name = "Finn"
dog.color = "blue"
dog.run = run

@animal
@dog
def puppy() -> None:
  ...

puppy()
Войти в полноэкранный режим Выйти из полноэкранного режима

Выполнение предыдущего фрагмента кода приведет к следующему результату:

An animal whose name: An animal and color: blue is currently running!
A dog whose name: Finn and color: blue is currently running!
Вход в полноэкранный режим Выйти из полноэкранного режима

Святые угольки! Что это вообще такое? Я просто спотыкаюсь, или весь язык — полная лажа? Это приводит к тому, что я сомневаюсь в своем существовании.

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

Заключение.

🔝 Перейти к TOC.

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

Я не уверен, почему Гвидо решил, что наследовать все от класса объекта — это хорошая идея, потому что, как вы, возможно, знаете, это будет потреблять много оперативной памяти для выполнения относительно сложной программы на python.

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

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

Основной вывод:

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

На эту тему есть что обсудить в отношении производительности, масштабируемости, ремонтопригодности и т.д. Предположительно, я буду исследовать ее еще глубже, когда у меня будет на это время. А пока я с нетерпением жду ваших ответов. Что вы думаете? Есть ли существенная разница между двумя парадигмами в python?

Вот и все для сегодняшней статьи. Спасибо за чтение! Будьте здоровы, друзья; увидимся в следующей статье.

Изображение Gerd Altmann с Pixabay

Ссылки и ресурсы.

🔝 Go To TOC.

[0] docs.python.org, python 3.10.4. Управление памятью.

[1] Барри Варшава, 2000. PEP 232 — Атрибуты функций.

[2] Википедия, свободная энциклопедия. Объектно-ориентированное программирование.

[3] Википедия, свободная энциклопедия. Функциональное программирование.

Оцените статью
Procodings.ru
Добавить комментарий