Вот в чем вопрос
Программисты никогда не могут договориться ни о чем, но, безусловно, один из самых больших аргументов, которые постоянно беспокоят интернет, — это битва между объектно-ориентированным программированием (ООП) и функциональным программированием (ФП).
Напоминаем, что ООП вращается вокруг упаковки всей вашей бизнес-логики и данных в классы, которые затем могут создавать объекты с той же функциональностью. Он также включает такие понятия, как наследование и полиморфизм, упрощающие создание классов с похожей, но слегка измененной функциональностью.
Язык, обычно используемый для демонстрации ООП, — это Java. В Java все должно быть заключено в класс, включая основной цикл выполнения вашей программы.
С другой стороны, функциональное программирование больше связано с — как вы уже догадались — функциями. В функциональном программировании данные часто передаются от функции к функции, каждая из которых выполняет операцию над данными. Функции часто разрабатываются таким образом, чтобы выдавать один и тот же результат при одинаковых входных данных.
Наиболее популярными языками функционального программирования являются Clojure, Elixir и Haskell.
Но как насчет Питона?
Python — интересный случай. Он имеет много общих черт с объектно-ориентированными языками, позволяя вам создавать классы и наследовать от суперклассов, но также обладает функциональностью, которую вы обычно видите в функциональных языках. Вам разрешено определять функции в основном теле программы, а функции также являются гражданами первого класса, то есть вы можете передавать их как объекты.
Правда в том, что Python очень гибкий. Если вы пришли с Java и хотите писать все в чисто объектно-ориентированном стиле, вы сможете выполнить большую часть того, что хотите. Если вы ранее работали с Clojure, у вас не возникнет особых проблем с воспроизведением шаблонов FP в Python.
Однако прелесть Python в том, что вы не ограничены ни одним из способов ведения дел. Вы можете использовать функции обеих парадигм для создания удобочитаемого, расширяемого кода, который будет поддерживать вашу кодовую базу даже по мере ее роста.
Ниже приведены три примера одной и той же (очень простой) программы, написанной на ООП, ФП и более Pythonic-смесь того и другого. Я выделю сильные и слабые стороны каждого из них, что должно дать вам хорошую основу при разработке вашего следующего проекта Python.
Программа
Программа, использованная для демонстрации, очень проста — она создает двух животных (собаку и рыбу) и заставляет их выполнять очень простые действия. В этом примере действия просто регистрируются в stdout
, но они, очевидно, могут делать намного больше.
Пример ООП
from abc import ABC, abstractmethod class Logger(ABC): @abstractmethod def log(self, message: str): ... class MyLogger(Logger): def __init__(self, name: str): self.name = name def log(self, message: str): print(f'{self.name}: {message}') class Animal: def __init__(self, name: str, logger: Logger): self.name = name self.logger = logger def speak(self): self.logger.log('Speaking') ... class Dog(Animal): def speak(self): self.logger.log('Woof!') ... def run(self): self.logger.log('Running') ... class Fish(Animal): ... class App: @staticmethod def run(): fido = Dog(name='Fido', logger=MyLogger('Fido')) goldie = Fish(name='Goldie', logger=MyLogger('Goldie')) fido.speak() fido.run() goldie.speak() if __name__ == '__main__': App.run() # Fido: Woof! # Fido: Running # Goldie: Speaking
Как видите, код создает класс MyLogger
для регистрации событий в stdout
, базовый класс Animal
, а затем классы Dog
и Fish
для более конкретных животных.
Чтобы лучше следовать парадигме ООП, в ней также определяется класс App
с единственным методом run
, который запускает программу.
Что хорошо в ООП и наследовании, так это то, что нам не нужно определять метод speak
в классе Fish
, и он по-прежнему сможет говорить.
Однако, если бы мы хотели иметь больше животных, которые могли бы бегать, нам пришлось бы ввести класс RunningAnimal
между Animal
и Dog
, который определяет метод run
, и, возможно, аналогичный класс SwimmingAnimal
для Fish
, но тогда наши иерархии начинают становиться все больше и больше. сложный.
Кроме того, классы MyLogger
и App
здесь практически бесполезны. Каждый делает только одну вещь и на самом деле делает код немного менее читаемым. Их лучше вытащить в функции log
и main
(или run
).
Нам также пришлось создать абстрактный базовый класс Logger
исключительно для того, чтобы в коде можно было правильно указать тип, и чтобы пользователи нашего API могли передавать другие средства ведения журнала, если они хотят войти в другое место, кроме stdout
, или если они хотят войти в систему. с другим форматом.
Пример ПП
Просто на заметку — я менее знаком с ФП, чем с ООП, так что это может быть не самый похожий на ФП способ реализации такого поведения, но я собираюсь использовать именно его.
import functools from typing import Callable Logger = Callable[[str], None] def log(message: str, name: str): print(f'{name}: {message}') def bark( name: str, log_fn: Logger, ) -> (str, Logger): log_fn('Woof!') return name, log_fn def run( name: str, log_fn: Logger, ) -> (str, Logger): log_fn('Running') return name, log_fn def speak( name: str, log_fn: Logger, ) -> (str, Logger): log_fn('Speaking') return name, log_fn def main(): run( *bark( 'Fido', functools.partial(log, name='Fido'), ), ) speak( 'Goldie', functools.partial(log, name='Goldie'), ) if __name__ == '__main__': main() # Fido: Woof! # Fido: Running # Goldie: Speaking
Сразу видно, что наш класс Logger
стал удобным псевдонимом типа для Callable[[str], None]
. Мы также определили функцию log
для обработки нашей печати. Вместо того, чтобы определять классы для наших животных, мы просто определили функции, которые принимают имя животного и Logger
функцию.
Вы заметите, что функции run
, speak
и bark
также возвращают свое имя и аргументы функции регистрации, что позволяет объединить их в конвейеры, как мы сделали для run
и bark
для Fido.
Мы также переместили нашу логику в функцию main
, избавив от необходимости определять целый класс только для запуска нашей программы.
Чтобы обойти тот факт, что наша функция log
не соответствует типу Logger
, мы используем functools.partial
для создания частичной функции, которая соответствует. Это позволяет нам заменить наш регистратор чем угодно, если мы можем использовать частичную функцию, чтобы уменьшить его, чтобы он соответствовал нашему типу Logger
.
Однако, поскольку мы ни во что не инкапсулируем данные, если бы мы хотели добавить больше атрибутов нашим животным, нам, вероятно, пришлось бы начать использовать объекты dict
для их представления и передачи, но тогда всегда есть беспокойство, что словарь создан неправильно, и поэтому отсутствует ключ, на который опирается одна из наших функций.
Чтобы обойти это, нам нужно создать функции инициализации для наших животных, после чего код снова становится все более и более беспорядочным.
Немного того и другого
Итак, что произойдет, если мы объединим немного ООП с немного FP? Я собираюсь добавить еще несколько кусочков Pythonic, чтобы отойти от традиционных парадигм ООП и FP и, надеюсь, сделать код немного чище и легче для чтения.
from dataclasses import dataclass from functools import partial from typing import Callable Logger = Callable[[str], None] def log(message: str, name: str): print(f'{name}: {message}') @dataclass class Animal: name: str log: Logger def speak(self): self.log('Speaking') @dataclass class Dog(Animal): breed: str = 'Labrador' def speak(self): self.log('Woof!') def run(self): self.log('Running') @dataclass class Fish(Animal): ... def main(): fido = Dog('Fido', partial(log, name='Fido')) goldie = Fish('Goldie', partial(log, name='Goldie')) fido.speak() fido.run() goldie.speak() if __name__ == '__main__': main() # Fido: Woof! # Fido: Running # Goldie: Speaking
В этом примере я использую модуль Python dataclasses
, чтобы не писать конструкторы для своих классов. Это не только сокращает часть кода, который мне нужно написать, но и значительно упрощает добавление новых атрибутов в конце строки, если мне это нужно.
Как и в примере с ООП, у нас есть базовый класс Animal
с подклассами Dog
и Fish
. Однако, как и в примере с FP, я использую псевдоним типа Logger
и functools.partial
для создания регистраторов для животных. Это упрощается благодаря поддержке Python функций как граждан первого класса.
Кроме того, функция main
— это просто функция. Я никогда не пойму, почему Java такая, какая она есть.
Смешивание ООП и ФП в продакшене
Итак, я признаю, что этот пример был невероятно простым, и хотя он послужил хорошей отправной точкой для этого обсуждения, теперь я хотел бы дать вам пример того, как эти концепции используются в производстве, и я собираюсь использовать две мои любимые библиотеки Python: FastAPI и Pydantic. FastAPI — это облегченная платформа API для Python, а Pydantic — это библиотека для проверки данных и управления настройками.
Я не буду подробно вдаваться в эти библиотеки, но Pydantic эффективно позволяет вам определять структуры данных с помощью классов Python, а затем проверять входящие данные и получать к ним доступ через атрибуты объекта. Это означает, что вы не столкнетесь с проблемами, возникающими при работе со словарями, и вы всегда будете знать, что ваши данные находятся в том формате, который вы ожидаете.
FastAPI позволяет вам определять ваши API-маршруты как функции, оборачивая каждый из них декоратором (что очень похоже на концепцию FP), чтобы инкапсулировать вашу логику.
Ниже приведен пример того, как это можно использовать. Опять же, это простой пример, но он достаточно репрезентативен для того, что вы можете увидеть в продакшене.
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Baz(BaseModel): qux: int class Foo(BaseModel): bar: str baz: Baz @app.get('/foo') async def get_foo(name: str, age: int) -> Foo: ... # Some logic here return Foo( bar=name, baz=Baz(qux=age), ) # GET /foo?name=John&age=42 # { # "bar": "John", # "baz": { # "qux": 42 # } # }
Как видите, FastAPI использует способность Pydantic преобразовывать вложенные объекты в JSON для создания ответа JSON для нашей конечной точки. Декоратор app.get
также зарегистрировал нашу функцию get_foo
с объектом app
, что позволяет нам делать GET
запросов к конечной точке /foo
.
Я надеюсь, что вы нашли эту статью полезной. Я хотел бы услышать, что вы думаете и к какой парадигме вы склоняетесь при написании Python.
Очевидно, что это не единственный способ объединить FP и ООП в Python, и существует множество шаблонов проектирования, которые можно реализовать и улучшить при использовании такого сочетания.
Я буду писать о них в будущем, и, подписавшись на меня в Медиуме, вы ничего не пропустите. Я также пишу о Python и моих текущих проектах в Твиттере и (совсем недавно) также публикую о них в Mastodon.
Я уверен, что скоро увидимся!
-Исаак