ООП в Python
Список вопросов к Python собеседованию
Отличия процедурного подхода и ООП
Существуют два главных подхода к написанию программ:
- Процедурное программирование
- Объектно-ориентированное программирование (ООП)
Цель у этих подходов одна - сделать процесс программирования максимально эффективным. Но в ООП, в отличии от процедурного подхода, данные первичны, а код для обработки этих данных - вторичен.
- В процедурном подходе основой программы является функция. Функции вызывают друг друга и при необходимости передают данные. В программе функции живут отдельно, данные — отдельно.
- Основной недостаток процедурного подхода - сложность создания и поддержки больших программ. Наличие сотен функций в таких проектах очень часто приводит к ошибкам и спагетти-коду.
- В основе объектно-ориентированного программирования лежит понятие объекта. Объект совмещает в себе и функции и данные.
- Основное преимущество ООП перед процедурным программированием - изоляция кода на уровне классов, что позволяет писать более простой и лаконичный код.1
Классы и объекты в ООП
Объектно-ориентированное программирование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определённого класса, а классы образуют иерархию наследования.2
Класс
- описывает множество объектов, имеющих общую структуру и обладающих одинаковым поведением
- это шаблон кода, по которому создаются объекты. Т. е. сам по себе класс ничего не делает, но с его помощью можно создать объект и уже его использовать в работе
- Классы в Python – это тоже объекты
- Допустимо динамическое изменение и добавление атрибутов классов
- Для скрытия внутренних данных используются синтаксические соглашения
- Поддерживается наследование
- Полиморфизм обеспечивается виртуальностью всех методов
- Доступно метапрограммирование
Объект (экземпляр класса)
- это конкретный представитель класса
- Жизненным циклом объекта можно управлять
- Многие операторы могут быть перезагружены
- Многие методы встроенных объектов можно эмулировать
| |
Атрибуты класса (Attributes)
Атрибут класса (объекта) - любой элемент (свойство, метод, подкласс), на который можно сослаться через символ точки (MyClass.<атрибут> или my_object.<атрибут>).
Атрибуты делятся на встроенные и пользовательские:
Встроенные (служебные) атрибуты - методы и свойства унаследованные от общего для всех классов в Python родительского класса
object. Многие из этих атрибутов можно переопределить внутри своего класса.Пользовательские атрибуты - поля и методы, которые описываются программистом в теле класса. Добавляются в общий список атрибутов наряду со встроенными.3
Поля класса - это характеристики объекта класса.
Методы класса - это функции, с помощью которых можно оперировать данными класса.
Любой метод является атрибутом, но не любой атрибут - методом.
Атрибуты-поля можно условно разделить на две группы:
- Статические - поля класса, которые объявляются внутри тела класса и создаются тогда, когда создается класс.
- Динамические - поля экземпляра. Для создания динамического поля необходимо обратиться к
selfвнутри метода.
| |
Служебное слово
self- это ссылка на текущий экземпляр класса.selfне является зарезервированным. Является аналогом этогоthis(Java, C++).
Встроенные (специальные) атрибуты
Атрибуты (поля и методы), имена которых обрамляются __, Python трактует как специальные. Специальные атрибуты, как правило, идут первыми при объявлении класса.
Использование:
- операторы перегрузки - если необходимо добавить возможность выполнения стандартных операций над классами
- дополнить…
Основные встроенные методы и поля:
__new__(cls, ...) - Конструктор. Создает экземпляр класса. Сам класс передается в качестве аргумента. Редко переопределяется, чаще используется реализация от базового класса object
__init__(self, ...) - Инициализатор. Принимает свежесозданный объект класса из конструктора. Является очень удобным способом задать параметры объекта при его создании.
__del__(self) - Деструктор. Вызывается при удалении объекта сборщиком мусора
__str__(self) - Возвращает строковое представление объекта.
__repr__
__hash__(self) - Возвращает хэш-сумму объекта.
__doc__ - Тип: str. Документация класса.
__dict__ - Тип: dict. Словарь, в котором хранится пространство имен класса
Свойства класса (@property)
Организация доступа к членам класса в Python построена на принципе универсального доступа, гласящем, что «все услуги, предлагаемые модулем должны быть доступны через единую нотацию, которая не раскрывает, реализованы ли они посредством хранения либо вычисления».
- предоставлять доступ к переменным напрямую, например,
foo.x = 0, а неfoo.set_x(0); - в случае необходимости проверки устанавливаемого значения использовать свойства, которые сохраняют единый синтаксис доступа, установка значения
foo.x = 0приводит к вызовуfoo.set_x(0).
Преимуществом данного подхода является возможность использование синтаксиса foo.x += 1, хотя на самом деле внутри происходит вызов foo.set_x(foo.get_x() + 1).
Свойства (Property) — это особый вид атрибутов имитирующий поле (но который при чтении вызывает какой-либо метод).
- У них есть методы получения, установки и удаления, такие как
__get__,__set__и__delete__ - Мы можем определить геттеры, сеттеры и деструкторы с помощью функции
property() - Свойство может определяться при помощи декораторов:
@property- определяет метод получения значения,@field.setter- определяет метод установки значения свойстваfield. Имя свойстваfieldопределяется в наименовании обоих методов и декораторе@field.setter - Если необходимо реализовать свойство «только для чтения», второй метод может быть опущен вместе с декоратором
@field.setter - Примером применения свойства является получение информации, которая может потребовать затратного первоначального поиска и простого повторного
| |
По сути, когда Python встречает следующий код:
| |
он ищет eggs в spam, а затем проверяет eggs на наличие у него методов __get__, __set__ или __delete__ и если они есть, то это свойство. Если это свойство, то вместо того, чтобы просто вернуть объект eggs (как это было бы для любого другого атрибута), он вызовет метод __get__ и возвращает все, что возвращает этот метод.4
Методы экземпляра (обычные методы)
Методы экземпляра - это обычные функции, которые становятся доступны только после создания экземпляра класса. Первым параметром такого метода является слово self.
Статические методы (Static methods)
Статические методы - это обычные функции, которые помещены в класс для удобства и тем самым располагаются в области видимости этого класса. Чаще всего это какой-то вспомогательный код.
Обозначаются специальным декоратором
@staticmethod.
- ничего не знают о классе или об объекте, на котором они вызываются
- не принимают специальных аргументов типа
selfилиclsпоэтому не используют сам объект или класс при выполнении - могут быть вызваны, как через сам класс, так и через его экземпляр
Методы класса (Class methods)
Методы класса принимают в качестве первого параметра cls (вместо self в обычных методах). cls - это ссылка на класс, на котором был вызван метод.
Обозначаются специальным декоратором
@classmethod.
- могут менять состояние самого класса, что в свою очередь отражается на ВСЕХ экземплярах данного класса
- не могут менять конкретный объект класса
- используются, когда не требуется привязка к экземпляру объекта
- привязаны только к области видимости
Методы класса часто используются, когда:
- Необходимо создать специфичный объект текущего класса
- Нужно реализовать фабричный паттерн (создаём объекты различных унаследованных классов прямо внутри метода)
- Реализовать дополнительные методы инициализации
Перегрузка методов (Method Overloading)
Перегрузка методов (Множественная диспетчеризация, Мультиметоды) - это использование множества методов с одним и тем же именем, которые позволяют выбирать нужную функциональность в зависимости от количества, типов или значений аргументов в пределах одного класса.
Python по умолчанию не поддерживает перегрузку методов, поскольку запоминает только самое последнее определение метода. Для их реализации необходимо подключать сторонние Python библиотеки, например, multimethods.py.
Переопределение методов (Method Overriding)
Когда метод с тем же именем и аргументами используется как в производном классе, так и в базовом или суперклассе, мы говорим, что метод производного класса переопределяет метод, представленный в базовом классе.
Последовательность поиска атрибутов
У одного объекта может быть несколько родительских классов, а также специальные методы вроде __getattribute__, которые перехватывают запросы к атрибутам.
Каким же образом интерпретатор разрешает сложные запросы к свойствам и методам? Рассмотрим последовательность поиска на примере запроса obj.field:
- Вызов
obj.__getattribute__('field'), если он определен. При установке или удалении атрибута проверяется соответственно наличие__setattr__или__delattr__. - Поиск в
obj.__dict__(пользовательские атрибуты). - Поиск в
object.__class__.__slots__. - Рекурсивный поиск в поле
__dict__всех родительских классов. Если класс имеет несколько предков, порядок проверки соответствует порядку их перечисления в определении. - Если определен метод
__getattr__, то происходит вызовobj.__getattr__('field') - Выбрасывается исключение несуществующего атрибута –
AttributeError.
Наконец, когда атрибут нашелся, проверяется наличие метода __get__ (при установке – __set__, при удалении – __delete__).
Все эти проверки совершаются только для пользовательских атрибутов.5
Класс как структура данных
В ряде случаев бывает полезным иметь структуру, похожую на структуру из языка Си (или запись из Паскаля), позволяющую логически сгруппировать данные. Для этого можно использовать словарь или класс с пустой реализацией.
Основные принципы ООП
На текущий момент ООП является самой востребованной и распространенной парадигмой программирования. Концепция ООП строится на основе 4 принципов: абстракция, инкапсуляция, наследование и полиморфизм.
Абстракция
Абстракция - принцип ООП, согласно которому объект характеризуется свойствами, которые отличают его от всех остальных объектов и при этом четко определяют его концептуальные границы.
Абстракция позволяет представить сложную концепцию в более простой форме:
- Выделить главные и наиболее значимые свойства предмета.
- Отбросить второстепенные характеристики.
Абстракция не поддерживается в Python напрямую.
Инкапсуляция
Инкапсуляция - принцип ООП, согласно которому сложность реализации программного компонента должна быть спрятана за его интерфейсом.
Инкапсуляция не дает взглянуть на внутреннюю реализацию сложной концепции:
- Отсутствует доступ к внутреннему устройству программного компонента.
- Взаимодействие компонента с внешним миром осуществляется посредством интерфейса, который включает публичные методы и поля.
В ряде языков, например, С++, существует четкое разделение членов класса на закрытые (private), защищенные (protected) и публичные (public).
В Python все члены класса являются общедоступными, но существует возможность эмуляции private и protected на уровне договоренностей.
Концепция отсутствия закрытых атрибутов в Python описывается фразой одного из разработчиков языка: «Мы все взрослые люди. Если программист хочет выстрелить себе в ногу - нужно предоставить ему возможность это сделать».
В Python принята следующая договоренность:
Protected (Non-Public) - обозначается при помощи одинарного нижнего подчеркивания _. Данный синтаксис указывает на то, что атрибут:
- используется для внутренней реализации класса и не предназначен для использования извне;
- должен быть использован/изменен только если разработчик-пользователь класса абсолютно уверен в этом.
- При этом атрибут с
_доступен извне, как и обычный public-атрибут класса.
Private - обозначается при помощи двойного нижнего подчеркивания __. Данный синтаксис указывает на то, что атрибут:
- используется для внутренней реализации класса и не предназначен для использования извне;
- не должен быть использован/изменен разработчиком-пользователем класса.
- При этом атрибут с
__оказывается недоступным извне, используя технику сокрытия имен (Name Mangling). Несмотря на это, в отличие от ряда языков (например, Java) такие «закрытые» члены класса также можно изменять, но более сложным способом - их можно увидеть, используя функциюdir().
We don’t use the term “private” here, since no attribute is really private in Python (without a generally unnecessary amount of work).6
| |
Геттеры, сеттеры и деструкторы
Кроме прямого доступа к атрибутам (obj.attrName), могут быть использованы специальные методы доступа: геттеры, сеттеры и деструкторы:
| |
Такой подход очень удобен, если получение или установка значения атрибута требует сложной логики.
Вместо того чтобы вручную создавать геттеры и сеттеры для каждого атрибута, можно перегрузить встроенные методы __getattr__, __setattr__ и __delattr__. Например, так можно перехватить обращение к свойствам и методам, которых в объекте не существует:
| |
__getattribute__ перехватывает все обращения (в том числе и к существующим атрибутам).
Для чего нужна инкапсуляция?
- Инкапсуляция упрощает процесс разработки, т. к. позволяет нам не вникать в тонкости реализации того или иного объекта.
- Повышается надежность программ за счет того, что при внесении изменений в один из компонентов, остальные части программы остаются неизменными.
- Становится более легким обмен компонентами между программами.
Наследование
Наследование - способ создания нового класса на основе уже существующего, при котором класс-потомок заимствует свойства и методы родительского класса, а также добавляет собственные.
- Класс потомок может переопределять родительские методы.
- При этом, обычно, дочерний класс дополняет родительский метод, добавив свой код после кода родителя (используя функцию
super(), предоставляющую ссылку на родительский класс). - Каждый класс также может получить информацию о своих «родителях» через метод
__bases__()илиisinstance(). - Наследование описывается словом «является» (легковой автомобиль является автомобилем).
- Существуют и другой вид взаимосвязи (модель включения/делегации), когда один класс включает в себя другой класс в качестве одного из полей - ассоциация, композиция и агрегация. Ассоциация описывается словом «имеет» (автомобиль имеет двигатель).
| |
Метод
super()дает возможность наследнику обратиться к родительскому классу.
Для чего нужно наследование?
- Принцип DRY (повторное использование кода);
- Классы-потомки берут общий функционал у родительского класса.
- Ускорение разработки нового ПО на основе переиспользования существующих открытых классов.
- Наследование упрощает процесс написания кода.
Python реализует как стандартное одиночное наследование так и множественное.
Множественное наследование
Используя множественное наследования можно создавать классы-миксины (примеси), представляющие собой определенную особенность поведения.
Множественное наследование часто критикуется 10 и зачастую считается признаком неверного анализа и проектирования, поэтому его использование рекомендуется в случае крайней необходимости и оправданности такого решения.7
Полиморфизм
Термин «полиморфизм» происходит из греческого языка и означает «нечто, что принимает несколько форм».
Полиморфизм - это поддержка нескольких реализаций на основе общего интерфейса.
Абстрактный метод (виртуальный метод) - это метод класса, реализация для которого отсутствует.
Все методы в языке изначально виртуальные. Это значит, что дочерние классы могут их переопределять и решать одну и ту же задачу разными путями. Название метода остается прежним, а реализация изменяется и будет выбрана только во время исполнения программы. Такие классы называют полиморфными.
| |
Можно получить и доступ к методам класса-предка либо по прямому обращению, либо с помощью функции super():
| |
Одинаковый интерфейс с разной реализацией могут иметь и классы, которые не связаны отношениями Родитель-Потомок. Это возможно благодаря утиной типизации.
Метаклассы
Метаклассы – это классы, инстансы которых тоже являются классами.8