Как вы вероятно знаете, у меня есть цикл материалов по ООП, где описаны все основные концепции. Но они описаны без привязки к какому-либо конкретному языку. Некоторые из них есть в одних языках и отсутствуют в других. Там, где их нет, приходится придумывать другие решения.
Мы пройдёмся по разным языкам и рассмотрим, как в них реализована каждая из концепций ООП. Первым языком станет Python.
1. Объект
Здесь мы рассматриваем динамическое создание безымянного объекта. Эталонная реализация есть, например, в JavaScript и использует JSON-подобную нотацию:
var o = { a:5, b:10 };
Мы создали простейший объект o, у которого есть свойства o.a и o.b.
Что нам предлагает Питон? Написав вот так:
o = { 'a':5, 'b':10 }
Мы вроде тоже получим объект. Но это не просто объект, а словарь (dict) в терминологии Питона, для остального же мира это ассоциативный массив, который хранит пары ключ → значение. Ключами в данном случае являются имена свойств: 'a', 'b'.
Доступ к псевдо-свойствам такого объекта будет выглядеть как o['a'] или o['b'].
Кроме того, объект может содержать и методы. Как это делается в JavaScript:
Как это можно сделать в Питоне через словарь:
Как видим, в JavaScript анонимная функция создаётся прямо внутри создаваемого объекта. В Питоне мы сделали отдельную функцию test и поместили указатель на неё в словарь под ключом 'test'. Чтобы вызывать эту функцию, придётся писать:
o['test']()
Сравним с JavaScript:
o.test();
Всё это, конечно, не совсем то, что хотелось. Посмотрим на ещё один способ:
o = type('', (), {'a':5, 'b':10, 'test':test})()
Выглядит замороченно (язык с простым синтаксисом для новичков 🤣), давайте разберём:
type() это встроенная функция Питона, которая конструирует определённый класс "на лету". На вход она принимает три параметра:
type(name, bases, params)
Где name это имя класса, bases это список классов, от которых надо отнаследовать создаваемый класс, и params – свойства класса. Записав
o = type('', (), {'a':5, 'b':10, 'test':test})()
мы передали в type() пустое имя класса – '', пустой список родительских классов – (), и тот же самый объект-словарь со свойствами – { 'a':5, 'b':10, 'test':test }
В результате мы получили безымянный класс, который содержит свойства a, b и метод test. Но класс это ещё не объект, а только его описание. Чтобы создать объект, мы вызываем конструктор этого класса, дописав скобки () в конце всей конструкции:
o = type('', (), {'a':5, 'b':10, 'test':test})()
Теперь мы можем использовать объектную нотацию, то есть писать o.a, o.b и o.test(). Отмечу, что несмотря на повышенную громоздкость создания объекта с ней ещё можно жить, но методы объекта всё равно нельзя создать внутри него – они должны быть внешние, и мы помещаем в параметры объекта только ссылки на них.
2. Классы
С объявлением классов проблем нет:
Здесь мы воссоздали предыдущий объект, но используем уже не безымянный класс, а объявленный ранее, с именем Test. Плюс – теперь метод test() инкапсулирован внутри класса, а не глобальный. Однако есть особенность работы со свойствами класса, которую мы рассмотрим ниже.
3. Наследование
С наследованием тоже нет проблем:
Мы отнаследовали от класса Base класс Test, и теперь класс Test имеет доступ к свойству a, которое есть у Base. Опять же, со свойствами всё не так просто, будет описано ниже. Но наследование работает, как и должно.
4. Множественное наследование
Мы отнаследовали класс Test от двух классов Base1 и Base2. Он имеет доступ к свойству a класса Base1 и свойству b класса Base2.
5. Конструктор
Конструктором является метод со специальным именем __init__, в котором мы можем буквально сконструировать объект, а именно создать собственные свойства объекта (a и b). Результат ничем не отличается от предыдущих, но разница есть, и о ней ниже.
6. Статические свойства и методы
Все свойства, объявленные в классе, являются статическими:
Также являются статическими методы, перед которыми написана директива @staticmethod. Это позволяет обращаться к свойствам и методам класса без создания объекта.
Когда вы создали объект из класса, вы можете написать o.a, и получите значение статического свойства a из класса. Оно существует не в объекте, а в классе. Но написав:
o.a = 0
вы не измените статическое свойство a в классе Test, а создадите своё, собственное свойство a в объекте o. То же самое происходит, когда вы создаёте собственное свойство объекта в конструкторе (см. выше). То есть вы можете обращаться к статическим свойствам класса через объект до тех пор, пока не попытаетесь их изменить. После изменения вы получите локальное свойство с тем же именем.
Присваивание вида Test.a = 0, то есть через имя класса, изменяет само статическое свойство класса, как и положено.
7. Инкапсуляция
В Питоне отсутствует деление на публичные и приватные свойства и методы. Культура Питона (типа другие бескультурные) продвигает подход "Нормально делай – нормально будет", то есть программист должен быть ответственным.
Но есть один (не приветствуемый) лайфхак, который позволяет затруднить доступ к свойствам и методам, которые вы хотите скрыть. Дело в том, что в Питоне имена, имеющие в начале два подчёркивания, автоматически расширяются следующим образом: если в классе Test есть имя __a, то его полное имя будет выглядеть так: _Test__a. Это сделано из-за внутренней кухни Питона, чтобы одинаковые имена внутри разных классов не путались между собой. Поэтому мы видим имя __a, но обратиться к нему извне не можем, потому что оно на самом деле _Test__a. А вот изнутри, то есть из пространства имён класса Test, запросто.
Здесь мы имеем класс Test со статическим свойством __a, с конструируемым свойством объекта __b, и наконец с "приватным" методом __test(). Конструктор __init__() тоже "приватный", понятно. Обращение типа o.__a или o.__test() даст ошибку, при этом вызов метода __test() из метода check_private() ошибки не даёт, так как вызов происходит изнутри класса. Однако доступ к "приватным" свойствам и методам всё же можно получить, написав их имена в полной форме: o._Test__a или o._Test__test(). Чем бы дитя ни тешилось...
Что насчёт protected методов? Их нет так же, как нет приватных. В идеологии Питона они не нужны, и вам предлагается просто использовать одно подчёркивание перед именем, чтобы визуально отличать их.
8. Абстрактные классы и методы
Поддержка абстрактных классов осуществляется с помощью модуля abc:
Всё это работает в версии Питона начиная с 3.4, более ранние версии рассматривать смысла нет.
Чтобы получить абстрактный класс, надо отнаследовать его от спец-класса ABC из модуля abc.
Чтобы сделать абстрактный метод, нужно перед ним написать @abstractmethod.
В результате мы получим типичное поведение абстрактного класса: из него нельзя создать объект, можно только отнаследоваться. Наследник должен реализовать у себя все абстрактные методы, иначе работать не будет.
9. Полиморфизм, Перезапись методов
Работает обычным образом. Для обращения к методу родителя из потомка используется super():
Для перегрузки метода можно указать необязательные параметры, присвоив им значения по умолчанию:
Ну это не совсем перегрузка, так как мы не меняем сигнатуру метода, а просто назначаем отcутствующим параметрам значения по умолчанию. Реальную перегрузку методов класса можно сделать с использованием модуля functools и начиная с версии 3.8:
Наш перегружаемый метод test() должен быть декорирован как @singledispatchmethod, а далее с помощью @test.register для него определяются несколько разных сигнатур. Почему это всё так сложно, я не знаю. Отметим, что параметры, передаваемые в перегружаемый метод, должны иметь тип, иначе их будет не различить.
Вообще говоря, тема dispatch-методов сильно обширнее и там можно углубиться во всякую дичь, но сейчас речь не об этом.
10. Интерфейсы
Их нет. Предлагается заменять абстрактными классами и duck-typing, то есть если вы намерены использовать класс Test в роли интерфейса, назовите его TestInterface и сообщество вас поймёт.
11. this
Для доступа объекта к самому себе в Питоне используется параметр self, который автоматически передаётся в метод самым первым:
Несмотря на то, что мы снаружи вызываем метод o.test(10), изнутри он получает сначала self, а потом 10. Название self не является частью языка. Вы можете назвать этот параметр как угодно, но в сообществе принято называть его self. И вообще говоря, self не обязательно должен указывать на объект, в котором объявлен метод. Метод объекта может быть динамически привязан к другому объекту, и тогда self будет определяться текущим контекстом.
Чтобы сделать метод геттером, надо декорировать его как @property. Чтобы сделать его сеттером, надо перед ним написать @a.setter, где a – имя метода, которое имитирует имя свойства. Само свойство a здесь сделано "приватным" с помощью лайфхака (см. выше в разделе "Инкапсуляция").
Что можно сказать в заключение? ООП в Питоне довольно развито, но некоторые вещи неочевидны или откровенно уродливо сделаны.
Читайте также: