LINUX.ORG.RU

Полиморфизм? - Можно кормить Зайку мясом?

 


0

3

Какие принципы ООП вы знаете?

Их четыре:
▫️наследование;
▫️инкапсуляция;
▫️полиморфизм;
▫️абстракция.

🔹Наследование
Наследование позволяет новому классу наследовать атрибуты и методы уже существующего класса. Новый класс называется производным (дочерним). Существующий — базовым (родительским).

🔹Инкапсуляция
Этот принцип заключается в ограничении доступа к внутренним методам и переменным класса извне. В Python принцип реализован лишь на уровне соглашений: приватные атрибуты выделяются подчёркиванием — одинарным _ или двойным __. Эти подчёркивания сигнализируют другим программистам о приватности. Однако доступ к ним всё равно можно получить. 

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

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

#вопросы_с_собеседований

Наследование - всё ясно. Из одного Зайки - можно сделать второго. Гораздо большего размера с разным цветом глаз - Свойства

Инкапсуляция - Зайку резать нельзя. Можно кормить - вход. Убирать дерьмо - выход. Зайка может прыгать - Методы.

Полиморфизм? - Можно кормить Зайку мясом? Или можно «прикрутить» к нему крылья?

Абстракция? - Зайка и Крокодил могут прыгать вместе? - Используя одно и тоже Свойство?

Перемещено leave из general



Последнее исправление: sokolov (всего исправлений: 1)
Ответ на: комментарий от monk

Балансированное дерево просто наследуется от обычного и меняется один метод. Как такое сделаешь без ООП?

Портянку не читал. У дерева есть некоторый интерфейс (набор из N функций, которые его принимают/возвращают — конструируют, преобразуют и т.д.), так ведь? Это протокол, определяем его (defprotocol).

Делаем два пользовательских типа данных, реализуем для них протокол — пишем N функций для простого дерева, потом ещё M < N для балансированного, реализация которых отличается; указываем, что вот этот набор из N функций реализует этот протокол для простого дерева, а вот этот — для балансированного (например, extend-protocol), причём большая часть функций в этих наборах одна и та же.

Примерно то же самое можно провернуть с мультиметодами, там даже пользовательские типы данных не нужны, достаточно обычных словарей (хотя в кложе и пользовательские типы данных — deftype/defrecord — по сути, те же самые словари — ассоциативные структуры данных); и диспетчеризация будет множественная — по произвольной функции от всех аргументов, а не только по типу и только первого аргумента. Правда, за гибкость придётся заплатить скоростью.

Зачем ООП? Зачем изменяемое состояние, доступ к которому инкапсулирован методами, намертво приколоченными к описанию структуры данных, размазанному по причудливой иерархии наследования? Это всё лишнее. Есть структуры данных, есть функции, которые ими оперируют — этого достаточно.

Nervous ★★★★★★★★★
()
Ответ на: комментарий от Nervous

Делаем два пользовательских типа данных, реализуем для них протокол — пишем N функций для простого дерева, потом ещё M < N для балансированного, реализация которых отличается.

А как компилятор узнает, что N функций для простого дерева работают на балансированном, если балансированное не наследник простого?

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

как компилятор узнает, что N функций для простого дерева работают на балансированном, если балансированное не наследник простого?

Ты ему скажешь, с помощью extend (или карамелизованных вариантов extend-type/extend-protocol).

Nervous ★★★★★★★★★
()
Ответ на: комментарий от Nervous

Ты ему скажешь, с помощью extend (или карамелизованных вариантов extend-type/extend-protocol).

Так это практически тот же ООП, только в с кучей бойлерплейта. И потенциальной проблемой, что все функции для дерева надо обязательно объявлять в протоколе, иначе они не будут работать для балансированного.

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

это практически тот же ООП

Не «тот же ООП», а «позволяет делать то же, что ООП» (только лучше — проще, понятнее и надёжнее, без лишнего говна и страданий). Согласись, есть разница.

только в с кучей бойлерплейта

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

Nervous ★★★★★★★★★
()
Ответ на: комментарий от Nervous

Зачем ООП? Зачем изменяемое состояние, доступ к которому инкапсулирован методами, намертво приколоченными к описанию структуры данных, размазанному по причудливой иерархии наследования? Это всё лишнее. Есть структуры данных, есть функции, которые ими оперируют — этого достаточно.

Почему намертво приколоченными? В CLOS методы можно описывать отдельно. И даже описывать методы для чисел, конкретного значения или произвольного класса.

А иерархия нужна, чтобы наследника можно было передать в любое место, где ожидают предка. Вот сделал ты балансированное дерево и есть функция для работы с деревьями. Без ООП тебе придётся или переписывать функцию или писать функцию-преобразователь (причём с копированием данных).

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

все функции для дерева надо обязательно объявлять в протоколе, иначе они не будут работать для балансированного

Что мешает определить одну функцию, а потом сослаться на неё в двух (трёх, восьми) разных реализациях протокола? Ничего не мешает.

Nervous ★★★★★★★★★
()
Ответ на: комментарий от monk

Почему намертво приколоченными? В CLOS методы можно описывать отдельно.

CLOS немного лучше, чем его более популярные собратья, да. Хотя от попыток разобраться в каше из :before, :after и :around и как они в каждом конкретном случае сочетаются, наверное, недолго и кукухой поехать %) Сделали из простых полиморфных функций какого-то франкенштейна, my ass.

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

Зачем? Любое значение, для типа которого реализован протокол (интерфейс), можно передать в любое место, которое ожидает этот интерфейс/протокол (использует только его функции/методы для работы с этим значением). На кой хрен тут ещё какое-то наследование?

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

Nervous ★★★★★★★★★
()
Последнее исправление: Nervous (всего исправлений: 2)
Ответ на: комментарий от Nervous

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

Только в ООП я пишу

(defclass tree () (left right))

(defmethod map-tree ((tree tree) f) ...)

(defmethod fold-tree ((tree tree) f init) ...)

(defmethod insert ((tree tree) node) ...)

(defmethod delete ((tree tree) node) ...)

(defclass balanced-tree (tree) ())

(defmethod insert ((balanced-tree balanced-tree) node) ...)

(defmethod delete ((balanced-tree balanced-tree) node) ...)

А с протоколами получается что-то вроде

(defprotocol Tree
  (map-tree [x f])
  (fold-tree [x f i])
  (insert [x n])
  (delete [x n]))

(def DefaultTree
  {:map-tree (fn [x f] ...)
   :fold-tree (fn [x f i] ...)})

(defrecord tree [left right])

(extend tree
  Tree
  (assoc DefaultTree
    :insert (fn [x n] ...)
    :delete (fn [x n] ...)))

(defrecord balanced-tree [left right])

(extend balanced-tree
  Tree
  (assoc DefaultTree
    :insert (fn [x n] ...)
    :delete (fn [x n] ...)))

Плюс, если в ООП я могу писать

(defmethod insert ((balanced-tree balanced-tree) node)
  (call-next-method)
  (rebalance balanced-tree))

то в кложе придётся копипастить.

monk ★★★★★★★★★★★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от Nervous

Зачем? Любое значение, для типа которого реализован протокол (интерфейс), можно передать в любое место, которое ожидает этот интерфейс/протокол. На кой хрен тут ещё какое-то наследование?

То есть для каждого типа надо делать дубль-протокол? Кложе повезло, что она динамическая и можно тип аргумента не писать.

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

То есть для каждого типа надо делать дубль-протокол?

Всмыслий? Вот у тебя есть функция foo, которая ожидает значение, реализующее протокол Frob с одним методом frob (у себя внутри она вызывает только этот метод для работы со своим аргументом). Для чего ты этот протокол реализуешь, то и сможешь передать в foo — хоть строку, хоть число, хоть словарь, хоть record, хоть nil.

можно тип аргумента не писать

Чем тебе Frob не тип, интересно.

Nervous ★★★★★★★★★
()
Последнее исправление: Nervous (всего исправлений: 1)
Ответ на: комментарий от monk

с протоколами получается что-то вроде

Множество вариантов есть с протоколами, можно оптимизировать по любому критерию, в том числе и по краткости.

в кложе придётся копипастить

Зачем копипастить, есть же обычная, стандартная, скучная композиция функций. Прошу заметить, не приколоченная к диспетчеризации %)

Хочешь наворотить в своей функции адовый конвейер обработки данных, с :before, :after, :around, :maybe и :dammit? На здоровьичко, как грится. Не хочешь? Ну и не делай, никто не заставляет. Свобода.

Nervous ★★★★★★★★★
()
Последнее исправление: Nervous (всего исправлений: 4)
Ответ на: комментарий от Nervous

Зачем копипастить, есть же обычная, стандартная, скучная композиция функций

А как? У нас есть insert для tree. Но передать balanced-tree туда нельзя.

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

Для каждого defrecord frob добавить defprotocol Frob. Чтобы можно было в foo требовать протокол.

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

Есть у нас тип Bar, мы хотим его передать в foo. Говно вопрос, реализуем для него протокол Frob, готово — foo работает с Bar (потому что он теперь ещё и немножечко Frob).

Можем реализовать Frob для хреновой тучи разных типов, можем реализовать хренову тучу разных протоколов для Bar. Open-closed principle, однако.

Nervous ★★★★★★★★★
()
Ответ на: комментарий от Nervous

Кстати, ещё одна проблема есть. Если tree и balanced-tree делают разные люди, то мы получаем жёсткую зависимость по именам полей. Если разработчик базового класса переименует поле, то у всех «наследников по протоколу» поломаются все унаследованные методы.

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

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

Они у любых наследников поломаются, по протоколу или по иерархии классов. Лекарство в обоих случаях одинаковое — не завязываться на имена полей (и вообще конкретную структуру данных), а завязываться на её интерфейс %)

Да, интерфейсы внутри интерфейсов, all the way down.

Nervous ★★★★★★★★★
()
Последнее исправление: Nervous (всего исправлений: 1)
Ответ на: комментарий от Nervous

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

Вот наш balanced-tree. У него есть функция rebalance, которой нет у tree. Есть функция bar, которая что-то делает с balanced-tree и выполняет rebalance. Ей надо дать аргумент с типом-протоколом. Протокол Tree давать нельзя, так как в нём нет rebalance. То есть нужен некий Balanced-tree. Кстати, методы в нём все дублировать или только новые?

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от Nervous

Они у любых наследников поломаются, по протоколу или по иерархии классов.

По иерархии классов они не пишутся в наследнике, поэтому не поломаются.

не завязываться на имена полей (и вообще конкретную структуру данных), а завязываться на её интерфейс

Так речь именно при реализацию интерфейса. DefaultTree очевидно будет реализовывать обработку через имена полей, так как в этот момент никакого другого интерфейса нет. А если имена полей в tree (и в DefaultTree поменяются), то balanced-tree получить сломанную реализацию интерфейса.

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

Вот наш balanced-tree. У него есть функция rebalance, которой нет у tree

Это значит, что у них разные интерфейсы (протоколы). Можно объявить протокол Tree (N функций) и протокол Balanced (одна функция rebalance).

Есть функция bar, которая что-то делает с balanced-tree и выполняет rebalance. Ей надо дать аргумент с типом-протоколом. Протокол Tree давать нельзя, так как в нём нет rebalance. То есть нужен некий Balanced-tree. Кстати, методы в нём все дублировать или только новые?

Чтобы bar работала с каким-то значением, достаточно, чтобы его тип реализовал протоколы Tree и Balanced — то есть чтобы к нему можно было успешно применить методы этих протоколов и получить осмысленный результат. Что это значение представляет собой «на самом деле» (тм), каков его «истинный тип» — её не колышет. Что за глупости вообще, какой истинный тип, there is no such thing — мы делим вещи по типам исходя из того, что собираемся с ними делать, у них нет никаких внутренне присущих им типов. Для одной задачи дерево — это стройматериал, для другой — топливо, для третьей — развлекательный аттракцион.

Таким образом, дублировать ничего не надо — берём наш тип, реализующий Tree, реализуем для него ещё и Balanced (+1 функция) — вуаля, bar работает с ним.

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

Nervous ★★★★★★★★★
()
Последнее исправление: Nervous (всего исправлений: 2)
Ответ на: комментарий от Nervous

Таким образом, дублировать ничего не надо — берём наш тип, реализующий Tree, реализуем для него ещё и Balanced (+1 функция) — вуаля, bar работает с ним.

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

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

для каждой структуры будет дополнительно свой протокол

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

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

Nervous ★★★★★★★★★
()
Ответ на: комментарий от Nervous

Протокол нужен для того, чтобы когда внутри bar встретится rebalance или insert, он был вызван именно для того типа, который передан в bar. А так как потенциально пользователи библиотеки структуру-наследник могут объявить для любой структуры, то протокол надо также делать для каждой структуры. И еще публиковать DefaultTree, DefaultBalanced, … чтобы при наследовании не приходилось копипастить.

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от Nervous

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

В классе меньше строк, чем в defrecord + defprotocol + Default*.

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от monk

Протокол нужен для того, чтобы когда внутри bar встретится rebalance или insert, он был вызван именно для того типа, который передан в bar.

Так он и будет вызван именно для того типа, для которого ты (или не ты) реализовал Tree и Balanced, а потом создал значение этого типа и передал в bar. Или для другого типа, который реализовал Вася из соседнего кабинета — но тоже реализующего Tree и Balanced. У протоколов диспетчеризация по типу первого аргумента, да.

Если ты передашь тип, который какой-то из этих протоколов не реализует, bar сломается.

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

Nervous ★★★★★★★★★
()
Ответ на: комментарий от monk

В классе меньше строк, чем в defrecord + defprotocol

Можно ещё попробовать класс зафигачить в одну строку, а defprotocol по одному символу в строке — так-то надёжнее будет! %)

Nervous ★★★★★★★★★
()
Последнее исправление: Nervous (всего исправлений: 1)
Ответ на: комментарий от monk

пользователи библиотеки структуру-наследник могут объявить для любой структуры, то протокол надо также делать для каждой структуры

Новая структура, новые возможности — новый интерфейс. Логично же.

Nervous ★★★★★★★★★
()
Ответ на: комментарий от monk

По иерархии классов они не пишутся в наследнике, поэтому не поломаются

Ты когда метод переопределяешь, к полям класса в нём принципиально не обращаешься? Если обращаешься (а почему нет, доступны же) и родитель их изменит, твой наследник сломается, разве нет?

Nervous ★★★★★★★★★
()
Ответ на: комментарий от Nervous

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

Нет. Я обосновываю тезис «то протокол надо также делать для каждой структуры».

monk ★★★★★★★★★★★★★★★
()
Ответ на: комментарий от Nervous

Ты когда метод переопределяешь, к полям класса в нём принципиально не обращаешься?

К родительским? Нет конечно. К родительскому классу только через интерфейс. Если добавляю поле, то к своему полю обращаюсь напрямую (хотя бы для того, чтобы интерфейс доступа к нему сделать).

а почему нет, доступны же

В Common Lisp доступно всё. Но есть правила хорошего тона: не обращаться напрямую к полям объекта и к неэкспортированным символам пакета.

monk ★★★★★★★★★★★★★★★
()