ООП в 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