Функция которая позволяет изменять парадигму
Перейти к содержимому

Функция которая позволяет изменять парадигму

  • автор:

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

Парадигма программирования это подход к конструированию программного обеспечения, основанный на нескольких определяющих принципах. Функциональное программирование это одна из таких парадигм, которая состоит из чистых функций (Pure Function), и позволяет избежать разделяемого состояния (Shared State), изменчивых данных (Mutable Data) и побочных эффектов (Side effect). Функциональное программирование является больше декларативным, чем императивным. Cостояние приложения в функциональном программировании, в отличие от объектно-ориентированного программирования, протекает через чистые функции.

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

Эта статья предоставит фундаментальное понимание функционального программирования и прояснит ряд его преимуществ. Чтобы начать понимать, что такое функциональное программирование, определим основные понятия.

  • чистая функция всегда возвращает одинаковое значение при одинаковых входах;
  • чистая функция не имеет побочных эффектов.

Предотвращение разделяемого состояния
Разделяемое состояние (Shared State) это любая переменная, объект или пространство памяти, которые существуют в общей области (shared scope), или свойство объекта, передаваемое между областями. Проблема с разделяемым состоянием состоит в том, что нужно знать всю историю каждой общей переменной, которую функция использует или затрагивает, чтобы понять результат вызова функции. Вторая проблема, связанная с общим состоянием, заключается в том, что изменение порядка вызова функций может вызвать ряд сбоев.
Функциональное программирование избегает разделяемое состояние, используя структуры неизменных данных и чистые вычисления для получения новых данных из уже существующих.

Неизменность
Неизменный объект (immutable object) это объект, который нельзя изменить после его создания. Если вы хотите изменить неизменяемый объект, лучше всего создать новый объект с новым значением. Неизменность это ключевое понятие функционального программирования. Без него теряется история состояний, и ошибки могут проникнуть в ваше программное обеспечение.

  • изменение любой внешней переменной или свойства объекта;
  • вход в консоль;
  • запись в файл.

Парадигмы программирования

Парадигмы программирования — это совокупность методов, концепций, принципов, техник и инструментов, которые определяют способ организации программы на языке программирования и ход её выполнения [1] .

Ещё парадигмами называют запрет на определённые действия внутри кода программы. Его придумал Роберт Мартин — международный консультант в области разработки, известный среди разработчиков как «дядя Боб». С его точки зрения, парадигмы — это ограничения на определённые языковые конструкции, которые вынуждают использовать определённый стиль. Например, процедурное программирование накладывает запрет на прыжки по коду программы, а функциональное — на прямое изменение памяти компьютера [2] .

Понятие

Парадигма программирования является концептуальной моделью, на основе которой организуется структура кода и происходит решение задач в программировании. Каждая парадигма определяет свой набор правил и подходов к написанию программ, поэтому разработчик выбирает парадигму в зависимости от требований задачи и уровня её сложности [3] .

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

История возникновения

В числе четырёх основных парадигм перечисляют процедурное программирование, структурное программирование, объектно-ориентированное программирование, функциональное программирование. Первым возникло функциональное программирование (Алонзо Чёрч, 1936 год), затем процедурное программирование (Фон Нейман, 1940-х годы), за ним объектно-ориентированное (Оле-Йохан Даль, Кристен Нюгор, начало 1960-х годов, Алан Кэй, 1967 год), последним — структурное программирование (Эдсгер Дейкстра, 1968 год) [4] .

Эволюция парадигм программирования

Знаменитый гуру программирования Гради Буч в 2001 году сделал важное признание в статье Through the Looking Glass: «Я был неправ: все-таки есть нечто более глубокое, нечто поистине более богатое возможностями, чем объекты». Этими словами Буч представил аспектно-ориентированное программирование (АОП) — на тот момент новейшую парадигму программирования.

История индустрии программного обеспечения началась с огромных машин и машинного кода — ни о чём не говорящих человеку рядов чисел, поэтому решение вычислительных задач с точки зрения проектирования и программирования было чрезвычайно трудоёмким. Требовались простые, удобные механизмы абстракции для упрощения процессов моделирования и программирования. Машинные языки сменил ассемблер, который в 1960-х — начале 1970-х уступил место структурным и процедурным языкам, позднее вытесненным объектно-ориентированным с его принципами инкапсуляции, наследования и полиморфизма.

Эволюция парадигм программирования происходила в связи с потребностью в разделении ответственности (separation of concerns): артефактов, концепций или особенностей, интересующих участников проекта разработки программного обеспечения. Зона ответственности может быть функциональной (например, добавление элемента к списку) или нефункциональной (например, быстродействие, безопасность и т. д.) либо относиться к этапам жизненного цикла разработки программного обеспечения, например к проектированию.

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

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

Типичная программа представляет собой набор взаимосвязанных друг с другом компонентов (например, отвечающих за управление транзакциями), обычно имеющих взаимозависимости по тем или иным функциям. Такое взаимосоединение называется сквозной функциональностью (cross-cutting concern), и аспектно-ориентированное программирование занимается её систематизацией, преобразованием в отдельные модули и их управлением.

Основным инструментом АОП являются аспекты — модули, реализующие сквозную функциональность. На сегодня существует несколько аспектно-ориентированных языков и инструментов — например, AspectJ и PostSharp.

С появлением АОП возникло немало вопросов о нужности и сферах применения новых парадигм, поэтому продуманный стандарт, предоставляющий логическую и систематизированную базу оценки и классификации парадигм, мог бы стать ценным подспорьем для участников разработки программного обеспечения [5] .

Разнообразие парадигм программирования

1. Процедурное программирование — это стиль программирования, при котором программа структурируется в виде набора процедур (функций или подпрограмм), которые выполняют конкретные задачи и могут вызываться из других частей программы; в процедурном программировании данные и функции обычно разделены, и данные передаются в процедуры в качестве параметров [6] .

2. Объектно-ориентированное программирование (ООП) — это стиль программирования, который основан на понятии объектов, включающих в себя данные и методы, которые манипулируют этими данными; ООП позволяет создавать программы, которые легко масштабировать и поддерживать, путем организации кода в виде объектов, которые взаимодействуют друг с другом [7] .

3. Функциональное программирование — это стиль программирования, в котором программа представляет собой набор функций, которые преобразуют данные.; функции являются основными строительными блоками программы, и они выполняют преобразования данных без побочных эффектов и изменения состояния программы [8] .

4. Логическое программирование — это стиль программирования, основанный на логике и математике; программы, написанные в логическом стиле, состоят из набора правил и фактов, которые позволяют вывести нужный результат из базы знаний [9] .

5. Исполнительное программирование — это стиль программирования, при котором программа создается напрямую в исполнимый код, без промежуточного этапа компиляции или интерпретации; в исполнительном программировании код программы создается напрямую в машинный код, который может быть выполнен процессором [10] .

6. Реактивное программирование — это стиль программирования, при котором программа реагирует на изменения внешних условий или внутренних состояний, путем автоматического запуска реакции на эти изменения; в реактивном программировании, программа организуется в виде потоков данных, которые передаются между различными компонентами программы, каждый из которых является отдельным потоком [11] .

7. Декларативное программирование — это стиль программирования, при котором программа описывает желаемый результат, а не последовательность команд для достижения этого результата; в декларативном программировании программист описывает, что программа должна делать, а не как её следует выполнить [12] .

8. Параллельное программирование — это стиль программирования, при котором задачи выполняются параллельно, одновременно на нескольких процессорах или ядрах процессора, чтобы ускорить работу программы; в параллельном программировании, задачи могут быть разделены на независимые подзадачи, которые выполняются параллельно [13] .

9. Аспектно-ориентированное программирование — это стиль программирования, который позволяет разделять код на модули, называемые аспектами, и внедрять эти аспекты в различные места кода; аспекты в аспектно-ориентированном программировании представляют собой модули, которые являются пересекающимися областями функциональности в приложении [14] .

10. Структурное программирование — это стиль программирования, в основе которого лежит представление программы в виде иерархической структуры блоков [15] .

11. Аппликативное программирование — это стиль программирования, в котором написание программы состоит в систематическом осуществлении применения одного объекта к другому [16] .

12. Обобщённое программирование — это стиль программирования, заключающийся в таком описании данных и алгоритмов, которое можно применять к различным типам данных, не меняя само это описание [17] .

13. Порождающее программирование — это подход, при котором код программы не пишется вручную, а создаётся автоматически программой-генератором на основе другой программы.

14. Агентно-ориентированное программирование — это стиль программирования, в котором основополагающими концепциями являются понятия агента и его ментальное поведение, зависящее от среды, в которой он находится [18] .

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

16. Автоматное программирование — это стиль программирования, при использовании которого программа или её фрагмент осмысливается как модель какого-либо формального автомата [19] .

17. Событийно-ориентированное программирование — это стиль программирования, в котором выполнение программы определяется событиями — действиями пользователя (клавиатура, мышь, сенсорный экран), сообщениями других программ и потоков, событиями операционной системы (например, поступлением сетевого пакета) [20] .

18. Компонентно-ориентированное программирование — это стиль программирования, существенным образом опирающийся на понятие компонента — независимого модуля исходного кода программы, предназначенного для повторного использования и развёртывания и реализующегося в виде множества языковых конструкций (например, «классов» в объектно-ориентированных языках программирования), объединённых по общему признаку и организованных в соответствии с определёнными правилами и ограничениями.

См. также

  • Программирование
  • Программист
  • Язык программирования
  • Программный код

Литература

  1. Панов С. А. Объектно-ориентированное программирование: Курс лекций / Панов С. А., Ганджа Т. В. — Томск: ТУСУР, 2015. — 110 с.
  2. Федотов А. М. Парадоксы информационных технологий // Вестник НГУ. Серия: Информационные технологии. — 2008. — Т. 6. — Вып. 2. — С. 3-14.
  3. Пальмер С. З., Фелсинг Дж. М. Практическое руководство по функционально-ориентированной разработке ПО: Пер. с англ. — М.: Вильямс, 2002. — 304 с. ISBN 5-8459-0365-3.
  4. Роганов Е. А. Основы информатики и программирования / Учебное пособие — М.: МГИУ, 2001. — 315 с., ил. ISBN 5-276-00187-1.

Примечания

  1. ↑Чистая архитектура. Часть II — Парадигмы программирования(рус.)(неопр.)?. Habr (26.04.2021). Дата обращения: 18 апреля 2023.
  2. ↑Что такое парадигмы программирования и зачем они нужны(рус.)(неопр.)?. Блог «Яндекс.Практикум» (18.01.2023). Дата обращения: 26 августа 2023.
  3. Себеста, Р. Концепции языков программирования. 9-е изд.. — Москва: ДМК Пресс, 2013. — 688 с. — ISBN 978-5-94074-806-2..
  4. Хариди, С. Язык программирования Оз: Введение в программирование.. — Москва: Символ-Плюс, 2008. — 400 с. — ISBN 978-5-93286-142-9..
  5. ↑Эволюция парадигмы программирования(рус.)(неопр.)?. Открытые системы (01.08.2012). Дата обращения: 26 августа 2023.
  6. Лафоре, Р. Объектно-ориентированное программирование в С++. — Санкт-Петербург: Питер, 2002. — 1040 с. — ISBN 5-318-00156-5.
  7. ↑ООП в картинках(неопр.) . Habr (16.08.2019). Дата обращения: 18 апреля 2023.
  8. ↑Функциональное программирование(рус.)(неопр.)?. GitHub. Дата обращения: 18 апреля 2023.
  9. Павловский М. Логическое программирование. — Санкт-Петербург: БХВ-Петербург, 2005. — 368 с. — ISBN 5-94157-379-1.
  10. Шилдт Г. Java 9. Руководство для начинающих. — Москва: Диалектика, 2018. — 816 с. — ISBN 978-5-8459-2089-5.
  11. ↑Реактивное программирование со Spring, часть 1 Введение(рус.)(неопр.)?. Habr (30.06.2021). Дата обращения: 18 апреля 2023.
  12. ↑Декларативный UI: определение, история и необходимость(рус.)(неопр.)?. Habr (17.11.2022). Дата обращения: 18 апреля 2023.
  13. ↑Введение в параллелизм(рус.)(неопр.)?. Habr (21.10.2021). Дата обращения: 18 апреля 2023.
  14. ↑Аспектно-ориентированное программирование: изучи и сделай сам!(рус.)(неопр.)?. Habr (28.10.2013). Дата обращения: 18 апреля 2023.
  15. Дал У. , Дейкстра Э. , Хоор К. Структурное программирование. — М. : Мир, 1975. — 247 с.
  16. Бэкус Дж. Можно ли освободить программирование от стиля фон Нейманна? Функциональный стиль и соответствующая алгебра программ / под ред. Р. Эшенхерста. — М. : Мир, 1993. — С. 84—158. — (Лекции лауреатов премии Тьюринга).
  17. Степанов А. А. , Роуз Д. Э. От математики к обобщённому программированию. — М. : ДМК Пресс, 2016. — 264 с. — ISBN 978-5-97060-379-6.
  18. Зайцев И. М. , Федяев О. И. Агентно-ориентированный подход к моделированию интеллектуальных распределённых систем. — Донецк: ДонНТУ, 2008. — С. 337—338.
  19. Поликарпова Н. И. , Шалыто А. А. Автоматное программирование. — СПб. : Питер, 2009. — 176 с. — ISBN 978-5-388-00692-9.
  20. Непейвода Н. Н. 13. Лекция: Событийное программирование // Стили и методы программирования. Курс лекций. Учебное пособие. — М. : Интернет-университет информационных технологий, 2005. — С. 213—222. — 316 с. — ISBN 5-9556-0023-X.

Ссылки

  • Страница «Парадигмы программирования: простое объяснение» на сайте «High Load»
  • Страница «Парадигмы программирования: какие бывают и на что влияют» на сайте «GeekBrains»
  • Страница «Что такое парадигмы программирования и зачем они нужны» на сайте «Skillbox Media»
  • Страница «Парадигмы программирования: определение, виды и их особенности» на сайте «CoderNet»
  • Страница «3 основные парадигмы программирования, которые вы должны знать» на сайте «Best Programmer»

Данная статья имеет статус «готовой». Это не говорит о качестве статьи, однако в ней уже в достаточной степени раскрыта основная тема. Если вы хотите улучшить статью — правьте смело!

  • Страницы, использующие волшебные ссылки ISBN
  • Знание.Вики:Готовые статьи по алфавиту
  • Знание.Вики:Готовые статьи о технологиях
  • Все статьи
  • Парадигмы программирования
  • Программирование

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

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

Рассказываю об одной из важнейших парадигм в программировании.

Парадигмы программирования и их виды

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

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

Существуют различные типы парадигм, например процедурный, ориентированный на работу с функциями, или логический, подразумевающий решение базовых логических задач в духе «если А = true, то и B = true». Но есть и более интересный подход к решению задач разработки, и это ООП-парадигма.

Комьюнити теперь в Телеграм
Подпишитесь и будьте в курсе последних IT-новостей

Что такое ООП?

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

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

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

Структура объектно-ориентированного программирования

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

Объекты

И хотя в структуре ООП объекты находятся не на первом месте, мы начнем с них, так как это упрощает общее понимание парадигмы.

Пример структуры в ООП на базе пользователя

Объект – это кусок кода, описывающий элемент с конкретным набором характеристик и функций. Например, вы делаете видеоигру, в которой есть персонаж. Ваш герой.

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

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

Методы

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

Атрибуты

Атрибуты – это конкретные характеристики объекта. Если вы хоть немного знакомы с программированием, то атрибуты можно представить в виде переменных с данными. Вернувшись к примеру с игровым персонажем, в качестве атрибутов можно представить характеристики в духе уровня выносливости, скорости и других статических показателей.

Классы

Это наиболее абстрактная и обобщенная форма в ООП. Что-то в духе шаблона, на базе которого строятся другие элементы структуры кода.

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

Поэтому мы можем создать класс – то есть объект, способный стать базой для других объектов. Например, класс – персонаж. Он умеет ходить, драться, имеет характеристики наподобие уровня здоровья или количества маны, то есть атрибуты, что есть у любых рас и классов в нашей РПГ. А уже человек-воин (объект) с ником Nagibator777 будет содержать более специфичные характеристики и методы, зависящие от решений игрока и других внешних факторов. Класс – это пример абстракции и наследования, упрощающий генерацию новых объектов.

Пример структуры в ООП на базе персонажа в игре

На картинках и схемах эта структура выглядит куда понятнее.

Ключевые принципы ООП

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

Инкапсуляция

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

Данные конкретного объекта или класса хранятся в пределах этого объекта или класса. Вносить в них изменения, используя другие классы, нельзя. У окружения есть право только запрашивать «публичные» методы и атрибуты.

Такой подход обеспечивает повышенный уровень безопасности, а также сокращает шансы на случайное повреждение данных внутри какого-то класса или объекта со стороны.

Наследование

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

Пример создания класса в JS

Это проще понять на примере со средствами передвижения:

  • Берем абстрактный класс «Средство передвижения» с возможностью набирать скорость и перевозить людей.
  • Из него формируем подкласс «Автобус», наследующий базовые характеристики и уточняющий их определенным количеством мест для людей и пределом скорости.
  • Затем создаем объект «Икарус» с более конкретной скоростью, планировкой, количеством дверей, типом сигнала и другими специфичными параметрами.

Не нужно каждый раз создавать новый класс или объект с полным набором опций. Достаточно воспользоваться конструкцией в духе export class Bus extends Vehicle() и дополнить код конкретикой.

Абстракция

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

Абстрактный класс должен оставаться публичным и не содержать реализации методов. Этим он отличается от дочерних классов.

Полиморфизм

Один из ключевых принципов ООП, позволяющий использовать одни и те же методы для обработки различных типов данных. Полиморфизм в разных языках программирования отличается: есть строго типизированные языки в духе C++, где задействуется «перегрузка», а есть такие языки, как JavaScript, где по умолчанию функции могут обрабатывать разные типы информации без необходимости указывать тип заранее.

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

Преимущества ООП

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

  • Модульность: инкапсуляция объектов в себе упрощает разработку, уменьшает количество ошибок и ускоряет разработку при участии большого количества программистов, так как каждый может работать независимо друг от друга.
  • Реюзабельность кода: благодаря абстракциям, полиморфизму и наследованиям можно не писать один и тот же код много раз, что заметно ускоряет создание нового ПО.
  • Высокая скорость разработки: классы и интерфейсы в ООП могут легко трансформироваться в подобие полноценных библиотек, которые можно переиспользовать в новых проектах.
  • Расширяемость: ООП-код легче развивать, дополнять и менять. Этому способствует независимая модульная структура.
  • Простота восприятия: использование ООП упрощает понимание кода за счет взаимодействия с объектами, а не логикой. Не нужно углубляться в то, как построено ПО, чтобы модифицировать его.
  • Безопасность: инкапсулированный код недоступен извне, поэтому «поломать» ООП-программу сложнее.
  • Гибкость: полиморфизм позволяет быстро адаптировать ООП-код под свои нужды, не описывая новые функции и объекты.

Недостатки ООП

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

Языки, исповедующие объектно-ориентированную парадигму

Существует множество языков программирования, подходящих для применения ООП-парадигмы. Среди них:

  • Ruby – высокоуровневый язык с динамической типизацией, созданный в середине 90-х японским разработчиком Юкихиро Мацумото.
  • С++ – статически типизированный язык программирования общего назначения, в первую очередь направленный на работу с ООП.
  • JavaScript – популярный язык с динамической типизацией, одинаково хорошо подходящий для различных парадигм разработки, включая ООП.

Также в число языков с акцентом на ООП-парадигме входят: C#, Python, Java, PHP, JADE, Scala и другие.

Вместо заключения

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

Основы функционального программирования на Python

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

  • Принципы функционального программирования
  • Оператор lambda , функции map , filter , reduce и другие
  • Включение в последовательность
  • Замыкание
  • Рекомендации по ФП на языке Python

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

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

В последние годы почти все известные процедурные и объектно-ориентированные языки программирования стали поддерживать средства функционального программирования (ФП). И язык Python не исключение.

Когда говорят о ФП, прежде всего имеют в виду следующее:

  • Функции – это «гражданеболее высокого сорта», т.е., все, что можно делать с «данными», можно делать и с функциями (в том числе передача функции другой функции в качестве аргумента).
  • Использование рекурсии в качестве основной структуры контроля потока управления. В некоторых языках не существует иной конструкции цикла, кроме рекурсии.
  • Акцент на обработке последовательностей. Списки с рекурсивным обходом подсписков часто используются в качестве замены циклов.
  • «Чистые» функциональные языки избегают побочных эффектов. Это исключает присваивания, почти повсеместно распространенный в императивных языках подход, при котором за одной и той же переменной последовательно закрепляются разные значения для отслеживания состояния программы.
  • ФП не одобряет или совершенно запрещает инструкции, используя вместо этого вычисление выражений (т.е. функций с аргументами). В предельном случае, одна программа есть одно выражение (плюс дополнительные определения).
  • ФП акцентируется на том, что должно быть вычислено, а не как.

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

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

Далее будут представлены несколько таких встроенных функций.

Оператор lambda, функции map, filter, reduce и другие

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

Оператор lambda

Помимо стандартного определения функции, которое состоит из заголовка функции с ключевым словом def и блока инструкций, в Python имеется возможность создавать короткие однострочные функции с использованием оператора lambda , которые называются лямбда-функциями. Вот общий формат определения лямбда-функции:

lambda список_аргументов: выражение

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

def standard_function(x, y): return x + y
lambda x, y: x + y

Но в отличие от стандартной функции, после определения лямбда-функции ее можно сразу же применить, к примеру, в интерактивном режиме:

>>> (lambda x, y: x+y)(5, 7) 12

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

>>> lambda_function = lambda x, y: x + y >>> lambda_function(5,7) 12 >>> func = lambda_function >>> func(3,4) 7 >>> dic = >>> dic['функция1'](7,8) 15

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

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

Функция map

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

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

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

>>> seq = (1, 2, 3, 4, 5, 6, 7, 8, 9) >>> seq2 = (5, 6, 7, 8, 9, 0, 3, 2, 1) >>> result = map(lambda_function, seq, seq2) >>> result >>> list(result) [6, 8, 10, 12, 14, 6, 10, 10, 10]

В приведенном выше интерактивном сеансе в строках 1 и 2 двум переменным, seq и seq2, присваиваются две итерируемые последовательности. В строке 3 переменной result присваивается результат применения функции map, в которую в качестве аргументов были переданы ранее определенная лямбда-функция и две последовательности. Обратите внимание, что функция map возвращает объект-последовательность map, о чем говорит строка 5. Особенность объекта-последовательности map состоит в том он может предоставлять свои элементы, только когда они требуются, используя ленивые вычисления. Ленивые вычисления – это стратегия вычисления, согласно которой вычисления следует откладывать до тех пор, пока не понадобится их результат. Программистам часто приходится обрабатывать последовательности, состоящие из десятков тысяч и даже миллионов элементов. Хранить их в оперативной памяти, когда в определенный момент нужен всего один элемент, не имеет никакого смысла. Ленивые вычисления позволяют генерировать ленивые последовательности, которые при обращении к ним предоставляют следующий элемент последовательности. Чтобы показать ленивую последовательность, в данном случае результат работы примера, необходимо эту последовательность «вычислить». В строке 6 объект map вычисляется во время преобразования в список.

Функция filter

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

В данном формате предикативная_функция – это ссылка на стандартную функцию либо лямбда-функция, которая возвращает истину либо ложь, и последовательность – это итерируемая последовательность, т.е. список, кортеж, диапазон или строковые данные.

Например, ниже приведена однострочная функция is_even для определения четности числа:

is_even = lambda x: x % 2 == 0

Чтобы отфильтровать все числа последовательности и оставить только четные, применим функцию filter :

>>> seq = (1, 2, 3, 4, 5, 6, 7, 8, 9) >>> filtered = filter(is_even, seq) >>> list(filtered) [2, 4, 6, 8]

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

>>> filtered = iter(filter(lambda x: x % 2 == 0, seq)) >>> list(filtered) [2, 4, 6, 8]

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

>>> next(filtered) 2 >>> next(filtered) 4 . 

Примечание . Для предотвращения выхода за пределы ленивой последовательности необходимо отслеживать возникновение ошибки StopIteration. Например,

seq = sequence try: total = next(seq) except StopIteration: return

Функция reduce

Наконец, когда требуется обработать список значений таким образом, чтобы свести процесс к единственному результату, для этого используется функция reduce. Функция reduce имеется в модуле functools стандартной библиотеки, но здесь она будет приведена целиком, чтобы показать, как она работает:

def reduce(fn, seq, initializer=None): it = iter(seq) value = next(it) if initializer is None else initializer for element in it: value = fn(value, element) return value

Вот общий формат функции reduce :

reduce(функция, последовательность, инициализатор)

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

Переданная при вызове функция вызывается в цикле для каждого элемента последовательности. Например, функция reduce может применяться для суммирования числовых значений в списке. Например, вот так:

>>> seq = (1, 2, 3, 4, 5, 6, 7, 8, 9) >>> get_sum = lambda a, b: a + b >>> summed_numbers = reduce(get_sum, seq) >>> summed_numbers 45

Вот еще один пример. Если sentences – это список предложений, и требуется подсчитать общее количество слов в этих предложениях, то можно написать, как показано в приведенном ниже интерактивном сеансе:

>>> sentences = ["Варкалось.", >>> . "Хливкие шорьки пырялись по наве, и", >>> . "хрюкотали зелюки, как мюмзики в мове."] >>> wsum = lambda aсс, sentence: aсс + len(sentence.split()) >>> number_of_words = reduce(wsum, sentences, 0) >>> number_of_words 13

В лямбда-функции, на которую ссылается переменная wsum , строковый метод split разбивает предложение на список слов, функция len подсчитывает количество элементов в получившемся списке и прибавляет его в накапливающую переменную.

В чем преимущества функций более высокого порядка?

  • Они нередко состоят из одной строки.
  • Все важные компоненты итерации – объект-последовательность, операция и возвращаемое значение – находятся в одном месте.
  • Программный код в обычном цикле может повлиять на переменные, определенные перед ним, или которые следуют после него. По определению эти функции не имеют побочных эффектов.
  • Они представляются собой элементарные операции. Глядя на цикл for , приходится построчно отслеживать его логику. При этом в качестве опоры для создания своего понимания программного кода приходится отталкиваться от нескольких структурных закономерностей. Напротив, функции более высокого порядка одновременно являются строительными блоками, которые могут быть интегрированы в сложные алгоритмы, и элементами, которые читатель кода может мгновенно понять и резюмировать в своем уме. «Этот код преобразовывает каждый элемент в новую последовательность. Этот отбрасывает некоторые элементы. А этот объединяет оставшиеся элементы в единый результат».
  • Они имеют большое количество похожих функций, которые предоставляют возможности, которые служат дополнением к их основному поведению. Например, any , all или собственные их версии.

Приведем еще пару полезных функций.

Функция zip

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

В данном формате последовательность – это итерируемая последовательность, т.е. список, кортеж, диапазон или строковые данные. Функция zip возвращает ленивый объект-последовательность, который нужно вычислить, чтобы увидеть результат. Приведенный ниже интерактивный сеанс это демонстрирует:

>>> x = 'абв' >>> y = 'эюя' >>> zipped = zip(x, y) >>> list(zipped) [('а', 'э'), ('б', 'ю'), ('в', 'я')]

В сочетании с оператором * эта функция используется для распаковки объединенной последовательности (в виде пар, троек и т.д.) в отдельные кортежи. Приведенный ниже интерактивный сеанс это демонстрирует:

>>> x2, y2 = zip(*zip(x, y)) >>> x2 ('а', 'б', 'в') >>> y2 ('э', 'ю', 'я') >>> x == ''.join(x2) and y == ''.join(y2) True

Функция enumerate

Встроенная функция enumerate возвращает индекс элемента и сам элемент последовательности в качестве кортежа. Вот общий формат функции enumerate:

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

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

>>> lazy = enumerate(['а','б','в']) >>> list(lazy) [(0, 'а'), (1, 'б'), (2, 'в')]

В строке 2 применена функция list , которая преобразовывает ленивую последовательность в список. Функция enumerate также позволяет применять заданную функцию к каждому элементу последовательности с учетом индекса:

>>> convert = lambda tup: tup[1].upper() + str(tup[0]) >>> lazy = map(convert, enumerate(['а','б','в'])) >>> list(lazy) ['А0', 'Б1', 'В2']

Функция convert в строке 1 переводит строковое значение второго элемента кортежа в верхний регистр и присоединяет к нему преобразованное в строковый тип значение первого элемента. Здесь tup – это кортеж, в котором tup[0] – это индекс элемента, и tup[1] – строковое значение элемента.

Включение в последовательность

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

squared_numbers = [x*x for x in numbers]

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

[выражение for переменная in список if выражение2]

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

>>> numbers = [1, 2, 3, 4, 5] >>> squared_numbers = [x*x for x in numbers] >>> squared_numbers [1, 4, 9, 16, 25]

Приведенное выше включение в список эквивалентно следующему ниже фрагменту программного кода:

>>> squared_numbers = [] >>> for x in numbers: >>> squared_numbers.append(x*x) >>> squared_numbers [1, 4, 9, 16, 25]

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

В конструкции включения в последовательность используется математическая запись построения последовательности. Такая запись в теории множеств и логике называется определением интенсионала множества и описывает множество путем определения условия, которое должно выполняться для всех его членов. В сущности, в терминах этих областей науки, выполняя данную операцию в Python, мы «описываем интенсионал» соответственно списка, словаря, множества и итерируемой последовательности. Ниже приведены примеры описания интенсионала соответственно списка, словаря, множества и итерируемой последовательности.

Таблица 1. Формы описания интенсионала

Выражение

Описание

[x*x for x in numbers]

set(x*x for x in numbers)

(x*x for x in numbers)

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

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

Таким образом, примеры из разделов о функциях map и filter легко можно переписать с использованием включения в последовательность. Например, в строке 3 приведенного ниже интерактивного сеанса вместо функции map применена операция включения в список:

>>> seq = (1, 2, 3, 4, 5, 6, 7, 8, 9) >>> seq2 = (5, 6, 7, 8, 9, 0, 3, 2, 1) >>> result = [x + y for x, y in zip(seq, seq2)] >>> result [6, 8, 10, 12, 14, 6, 10, 10, 10]

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

Включение в список применено и в приведенном ниже примере вместо функции filter :

>>> result = [x for x in seq if is_even(x)] >>> result [2, 4, 6, 8]

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

Замыкание

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

Используя замыкания, можно разделить исполнение функции со многими аргументами на большее количество шагов. Эта операция называется каррированием и обязана своим названием Хаскелю Каррингу. Каррирование – это преобразование функции многих аргументов в функцию, берущую свои аргументы по одному. Например, предположим, ваш программный код имеет приведенную ниже стандартную функцию adder :

def adder(n, m): return n + m

Чтобы сделать ее каррированной, она должна быть переписана следующим образом:

def adder(n): def fn(m): return n + m return fn

Это же самое можно выразить при помощи лямбда-функций:

adder = lambda n: lambda m: n + m

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

>>> sum_three = adder(3) >>> sum_three ..> >>> sum_three(1) 4

В приведенном выше примере каррированная функция adder(3) присваивается переменной sum_three, которая теперь на нее ссылается. Если вызвать функцию sum_three , передав ей второй аргумент, то она вернет результат сложения двух аргументов 3 и 1.

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

def power_generator(base): return lambda x: pow(x, base)

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

>>> square = power_generator(2) # функция возведения в квадрат >>> square(2) 4 >>> cube = power_generator(3) # функция возведения в куб >>> cube(2) 8

Отметим, что функции square и cube сохраняют значение переменной base . Эта переменная существовала только в среде power_generator , несмотря на то, что эти возвращенные функции абсолютно независимы от функции power_generator . Напомним еще раз: замыкание – это функция, которая имеет доступ к некоторым переменным за пределами своей собственной среды.

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

COUNT = 0 def count_add(x): global COUNT COUNT += x return COUNT

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

def make_adder(): n = 0 def fn(x): nonlocal n n += x return n return fn

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

>>> my_adder = make_adder() >>> print(my_adder(5)) # напечатает 5 >>> print(my_adder(2)) # напечатает 7 (5 + 2) >>> print(my_adder(3)) # напечатает 10 (5 + 2 + 3)

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

Рекомендации по ФП на языке Python

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

Программирование в функциональном стиле не тождественно функциональному программированию.

Что делает функции нечистыми?

  • Глобальные мутации, т.е. внесение изменений в глобальное состояние,
  • Недетерминированность функций, т.е. которые для одинаковых входных значений могут возвращать разные результаты, и
  • Операции ввода-вывода.

Пример глобальной мутации:

def append_one(xs): xs.append(1) return xs xs = [] print(append_one(xs)) # Результат: [1] print(append_one(xs)) # Результат: [1, 1]
import random print(random.random()) # Результат: 0.2334551699781765 print(random.random()) # Результат: 0.7073046734532323

Пример операции ввода-вывода:

with open('temp.txt', 'w') as f: f.write('Hello!') with open('temp.txt', 'r') as f: print(f.read()) # Результат: Hello! with open('temp.txt', 'w') as f: f.write('Hi!') with open('temp.txt', 'r') as f: print(f.read()) # Результат: Hi!

Из чистых функций вытекает ссылочная (референциальная) прозрачность. Говорят, что программа или математическое выражение ссылочно прозрачны, если любое подвыражение можно заменить его значением, и это не приведет к изменению значения целого, т. е. скрытые побочные эффекты отсутствуют. Математические рассуждения, преобразования и доказательства корректности могут быть справедливыми только для выражений, обладающих этим свойством. А программы, написанные на обычных императивных языках, не являются ссылочно прозрачными, так как присваивание значений глобальным переменным, в некоторых случаях и локальным, вызывает скрытые побочные эффекты.

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

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

Эффективное функциональное программирование на Python вполне возможно.

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

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

Указанные выше два простых правила делают ненужными архитектурные шаблоны и принципы ООП, заменяя их функциями! А что, спросите вы, и классы тоже? В Python использование классов не противоречит ФП, если в них отсутствует мутирующие интерфейсы.

Пример класса с мутирующим интерфейсом:

class Dog: def __init__(self, name, age): self.name = name self.age = age def bark(self): print('bark bark!') # !! def birthday(self): self.age += 1 # !! def setbuddy(self, buddy): self.buddy = buddy # !!

Пример класса без мутирующего интерфейса:

class Dog: def __init__(self, name, age): self.name = name self.age = age def description(self): return f' is years old' def speak(self): return f' says '

Но лучше использовать замороженные dataclasses и копирование, где необходимо. Иными словами, все классы должны быть замороженными dataclasses.

from dataclasses import dataclass @dataclass(frozen=True) class Position: name: str lon: float = 0.0 lat: float = 0.0 >>> pos = Position('Oslo', 10.8, 59.9) >>> pos.name 'Oslo' >>> pos.name = 'Stockholm' dataclasses.FrozenInstanceError: cannot assign to field 'name'

При всем при этом dataclasses могут быть вполне себе умными!

@dataclass(frozen=True) class Coord: row: int col: int def to_tupple(self) -> Tuple[int, int]: return self.row, self.col @classmethod def from_str(cls, str_coord: str) -> 'Coord': xl_row, xl_col = coordinate_ro_tuple(str_coord) return cls(xl_row - 1, xl_col - 1) def shift_down(self, shift: int) -> 'Coord': return self.__class__(self.row + shift, self.col)

Также следует использовать сторонние функциональные библиотеки (например, toolz), которые обеспечивают более оптимальную композиционность функций.

from toolz import pipe # h(g(f(data))) --> pipe(data, f, g, h)

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

И применять архитектурный шаблон «немутируемое ядро — мутируемая оболочка» (aka «функциональное ядро — императивная оболочка»), который позволяет выносить мутацию во вне и производить ее на границах приложения.

Выводы

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

Данный пост служит дополнением к моему предыдущему посту о конвейере данных. Приведенный выше материал был опубликован в качестве авторского в переводе книги Starting Out with Python и дополнен материалами Энтони Хвона.

  • python
  • функциональное программирование
  • map reduce
  • функциональная парадигма
  • программирование
  • читальный зал
  • clojure
  • Python
  • Программирование
  • Функциональное программирование
  • F#
  • Clojure

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *