Классы
И снова обратимся к Лутцу, он явно скажет лучше чем я: Классы – это основные инструменты объектно-ориентированного программирования (
ООП) в языке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.