Если вы следите за мной в 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] Википедия, свободная энциклопедия. Функциональное программирование.