Классы
И снова обратимся к Лутцу, он явно скажет лучше чем я: Классы – это основные инструменты объектно-ориентированного программирования (
ООП
) в языкеPython
, поэтому в этой статье мы попутно рассмотрим основыООП
.ООП
предлагает другой, часто более эффективный подход к программированию, при котором мы разлагаем программный код на составляющие, чтобы уменьшить его избыточность, и пишем новые программы, адаптируя имеющийся программный код, а не изменяя его.
Давайте еще определение ООП из Википедии:
Объе́ктно-ориенти́рованное программи́рование (
ООП
) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определённого класса, а классы образуют иерархию наследования.
- Объекты - данные + медоты(функции). Для создания объектов используются классы, все объекты являются экземпляром какого-то класса.
- Класс - объект, создающий новые объекты(экземпляры этого класса), объекты будут наследовать методы класса, которым они были порождены.
Некоторые утверждения
- В
python
все является объектами. А значит и классы тоже являются объектами, поэтому нам важно разделять объект класса(сам класс) и экземпляры класса(объекты порожденные этим классом)
Класс как пространство имен
Самому мне кажется что объяснение с этой стороны может путать, но давайте обозначим один момент, что классы чем-то похожи с модулями - они создают пространства имен, экземпляры классов тоже создают пространства имет, только пространство имен экземпляра связанно с пространством имен класса.
Создание классов
Для создание класса в python
используется инструкция class
. Она сильно похожа на объявление функций(def
) и так же как и функция является исполняющейся инструкцией, помимо этого так же как и def
, class
создает объект, только в этот раз не объект функции, а объект класса.
❗️ Важно
Среди разработчиков есть договоренность, называть классы с заглавной буквы, это помогает проще отличать их от обычных функций.
Мы с вами можем создать класс без методов и аттрибутов:
Представленный пример эквивалентен следующей записи:
Это значит, что любой класс является наследником класса object
и мы можем в этом убедиться используя глобальную функцию isinstance
:
Создание экземпляров
Для того чтобы создать экземпляр класса следует вызвать класс как функцию, с какими аргументами его вызывать мы посмотрим совсем скоро в теме про метод __init__
.
__init__
Функция-конструктор Помните мы говорили, что классы создают объекты? Но как они это делают? Как было сказанно до этого, экземпляр наследует пространство имен своего класса + если в классе имеется метод __init__
, эта функция будет вызываться для коструирования экземпляра класса. Такой метод называется конструктором. Функция-конструктор __init__
будет первым аргументом получать экземпляр класса, это аргумент можно называть как угодно, но принято называть его self
.
Как можно заметить из примера, функция __init__
получает не только self
, но и name
. Это значит, что вызывать ее мы можем с дополнительными аргументами, но как они передаются? Верно! Как видно из примера, аргументы передаются при вызове класса, если класс вызвать с аргументами, они будут переданы в __init__
.
Обратите внимание
Функция
__init__
не является какой-то "особенной", ее отличает только необычное название и первый позиционный аргумент, который автоматически будет подставлен. Поэтому все что вы знаете про функции применимо и к ней.
Поиск аттрибутов
Поиск аттрибутов в экземплярах/классах. Когда вы обращаетесь к аттрибуту, поиск значения этого аттрибута проводится по:
- Собственным аттрибутам экземпляра
- Аттрибутам класса
- Аттрибутам родительских классов
Мы хоть еще и не говорили про наследование, но давайте посмотрим следующий пример, в котором класс Dog
является подклассом Animal
и рассмотрим как будет происходить поиск аттрибутов.
Вроде бы понятно, но давайте я представлю парочку примеров на засыпку?
Пример 1
Пример 2
Присваивание аттрибутов
Для того чтобы присвоить значение в аттрибут объекта используется следующий синтаксис:
Либо существует 2 глобальные функции setattr(obj, name, value)
и getattr(obj, name, default = None)
.
Где хранятся аттрибуты объектов?
Почти у каждого объекта в python
, за некоторыми исключениями к объекту привязан словарь содержащий его аттрибуты. Посмотреть на это словарь мы можем с помощью функции vars()
которая возвращает магический аттрибут __dict__
объекта, в котором как раз и хранятся все аттрибуты объекта.
❗️ Внимание
Обращаю внимание, на то, что мы говорим про собственные аттрибуты объекта, и не касаемся операции поиска аттрибутов в дереве наследования.
Приватные методы
Приватные методы - те методы объекта, которые разработчик решил не предоставлять для использования другим людям, кто пользуется его классом. Есть некоторое соглашение между разработчиками что:
- Методы начинающиеся на букву(
some_method
) - доступны для использования. - Методы начинающиеся на одно подчеркивания(
_some_private_method
) пользовать самостоятельно не стоит.
Почему это могло ему понадобиться? Представьте что вы делаете класс которым надо пользоваться определенным способом... например класс отвечающий за отправку SMS.
Вообще к представленному коду можно прикопаться...
- Валидатор можно было бы выделить в отдельный класс
- Можно было бы так же выделить класс для
SmsGetaway
, который знает как отправлять смс'ки через определенный шлюз- Вообще зачем делать проверки и определять подходящий шлюз только в методе
send_sms
, а не в__init__
- А под каждое сообщение надо инициализировать какой-то экземпляр класса
SmsSender
? Вопросов много, но код я показываю только как пример того зачем нам нужны приватные методы.
Вы можете заметить, что нам предоставлен только конструктор и метод send_sms
, который производит обязательную работу перед отправкой(валидирует параметры и выбирает шлюз), будет крайне нежелательно если кто-то другой воспользуется этим классом и сразу вызовет метод _send_sms
мимо валидации.
Методы с двумя нижними подчеркиваниями
TODO: Описать зачем это надо
Магические методы
Это специальные методы, с помощью которых вы можете добавить в ваши классы «магию». Они всегда обрамлены двумя нижними подчеркиваниями (например, init или lt). Ещё, они не так хорошо документированны, как хотелось бы.
__init__() -> None
- функция-конструктор__del__() -> None
- функция-деконструктор__new__() ->
__str__() -> str
- переопределяет текстовое представление объекта__repr__() -> str
- очень похоже на str, но используется для различной отладочной информаци и должно возвращать представление в виде python кода
Переопределение операторов
Я перечислю не все методы, но те, которые могут быть полезны укажу.
Переопределение обращения к аттрибутам
__getattr__(self, name)
- переопределяет обращение к неизвестному аттрибуту__setattr__(self, name, value)
- переопределяет поведение для присвоения значения аттрибуту__delattr__(self, name)
- переопределяет поведение для удаления значения по аттрибуту__getattribute__(self, name)
- переопределяет оператор обращения ко всем аттрибутам
Переопределение поведения обращения по ключам
__getitem__(self, name)
- переопределение оператора обращения по ключу__setitem__(self, name, value)
- переопределение оператора добавления значения по ключу__delitem__(self, name)
- переопределение оператора удаления значения по ключу
@property
(setters/getters)
Декоратор Мы еще не говорили с вами про декораторы, но достаточно будет знать только то, что декораторы - это функции высшего порядка, получающие оригинальную функцию и возвращающие функцию обертки(не всегда, но чаще всего так).
Мы можем сделать так, что обращение к аттрибуту объекта будет вызывать метод, который называют getter
, а установка будет вызывать setter
. Для наглядности я напишу пример:
Наследование классов
Думаю, что сейчас увас должна сложиться картина про наследование, но мы с вами должны детально посмотреть на:
- На то как находится значение аттрибута в дереве наследования
- Что будет в
self
если метод был найден в родительском классе - Рассмотреть метод
mro()
у классов - Разобраться с функцией
super()
Давайте наконец поговорим с вами про self
в методах. Напоминаю, что это первый аргумент при вызове метода класса, подставляется он весьма просто... в момент обращения к аттрибуту вы уже получите не оригинальный метод, а обернутый метод который вызывает оригинал с подстановкой значения для аттрибута self
.
Method resolution order
У каждого класса есть специальный метод mro()
, который нам расскажет в какой последовательности будет происходить поиск аттрибутов.
Как видно из примера поиск сначала будет производиться в пространстве имен класса Animal
, потом в Base
, а уже в конце object
.
Пример с множественным наследованием:
Переопределение методов
Давайте рассмотрим эту тему ссылаясь на следующий код:
Помните, что дочерние классы просто наследуют пространство имен своих родителей? А поиск аттрибутов происходит вверх по дереву наследования, что будет если мы в дочернем классе(Subclass
) определим аттрибут с тем же именем что и у родительского(Superclass
)? Верно! При поиске аттрибутов мы получим уже не значение аттрибута из Superclass
а из Subclass
потому что он находится раньше в mro
.
Подобное поведение называется "переопределением метода". Давайте в коде попробуем это сделать
super()
Мы можем не только полностью переопределять методы ну и расширять методы родительских классов. Для этого мы воспользуемся функцией super()
super()
- предоставляет нам доступ к следующему классу в дереве наследования и если мы обращаемся к методу родительского класса подставляет в него значениеself
.