## Общая картина
В чем сущность ООП в Python? ООП предназначено для многократного использования кода.
В чем отличие между объектом класса и объектом экземпляра? Классы являются своего рода фабриками для создания множества экземпляров. Классы также поддерживают методы перегрузки операций, которые экземпляры наследуют, а любые функции, вложенные внутрь классов, трактуются как методы для обработки экземпляров.
### Поиск в иерархии наследования

На рисунке изображено дерево из пяти объектов, помеченных переменными, которые имеют присоединенные атрибуты, готовые для поиска. Более конкретно дерево связывает вместе три объекта классов (овалы Cl, С2 и СЗ) и два объекта экземпляров (прямоугольники I1 и I2), образуя дерево поиска в иерархии наследования. Обратитевнимание, что в объектной модели Python классы и генерируемые из них экземпляры являются двумя отдельными типами объектов.
С точки зрения деревьев поиска экземпляр наследует атрибуты от своего класса, а класс наследует атрибуты от всех классов выше в дереве.
Предположим, что мы построили дерево, приведенное на рисунке, и затем написали:
```
I2.w
```
Код сразу же обращается к наследованию. Поскольку он представляет собой выражение объект.атрибут, инициируется поиск в дереве, Python будет искать артибут в I2 и выше. В частности, он будет проводить поиск
внутри связанных объектов в следующем порядке:
```
I2, Cl, С2, СЗ
```
и остановится при нахождении первого присоединенного атрибута w (или сообщит об ошибке, если w не удалось отыскать). В данном случае атрибут w не будет найден до тех пор, пока не пройдет поиск в СЗ, потому что он присутствует только в этом объекте. Другими словами, благодаря автоматическому поиску I2 .w распознается как
СЗ.w.
В терминах ООП экземпляр I2 "наследует" атрибут w от СЗ.
В конечном итоге два экземпляра наследуют от своих классов четыре атрибута: w, х, у и z. Ссылки на другие атрибуты будут вызывать проход по другим путям в дереве.
```
class C2:
x = 'C2.x'
z = 'C2.z'
class C3:
w = 'C3.w'
z = 'C3.z'
class C1(C2, C3):
x = 'C1.x'
y = 'C1.y'
I1 = C1()
I2 = C1()
I2.name = 'hello'
```
Вот примеры.
• Для I1.х и I2.х атрибут х обнаруживается в С1 и поиск останавливается, т.к. С1 располагается в дереве ниже, чем С2.
• Для I1.у и I2.у атрибут у обнаруживается в С1, поскольку это единственное
место, где присутствует у.
• Для I1.z и I2.z атрибут z обнаруживается в С2, потому что С2 располагается в
дереве левее, чем СЗ.
• Для I2.name атрибут name обнаруживается в I2 вообще без подъема по дереву.
### Классы и экземпляры
Основное отличие между классами и экземплярами состоит в том, что классы представляют собой своего рода фабрики ддя генерирования экземпляров.
Есть еще одно отличие между классами и модулями — мы можем
иметь только один экземпляр отдельного модуля в памяти (именно потому модуль приходится перезагружать, чтобы получить его новый код), но для классов допускается создавать столько экземпляров, сколько нужно.
### Вызовы методов
Что происходит, когда мы пытаемся вызывать методы — функции, присоединенные к классам в качестве атрибутов.
Что делает Python, чтобы выполнить вызов метода bob.giveRaise().
0. Ищет giveRaise в bob с помощью поиска в иерархии наследования.
1. Передает bob найденной функции giveRaise в особом аргументе self.
### Создание деревьев классов
* Каждый оператор class генерирует новый объект класса.
* При каждом обращении к классу он генерирует новый объект экземпляра.
* Экземпляры автоматически связываются с классами, из которых они были созданы.
* Классы автоматически связываются со своими суперклассами в соответствии со способом их перечисления внутри круглых скобок в строке заголовка class; порядок слева направо здесь дает порядок в дереве.
Формально в примере применяется то, что называется множественным наследованием, которое просто означает наличие у класса более одного суперкласса выше в дереве классов — удобный прием, когда желательно объединить несколько инструментов. В Python при перечислении более одного суперкласса внутри круглых скобок в операторе class (как выше в С1) их порядок слева направо задает порядок, в котором будет производиться поиск атрибутов в этих суперклассах. По умолчанию используется крайняя слева версия имени, хотя вы всегда можете выбрать имя, запросив его у класса,где имя находится (скажем, СЗ. z).
### Перегрузка операций
Если в классе желательно гарантировать, что атрибут вроде name всегда устанавливается в экземплярах, тогда более типично будет заполнять его во время конструирования:
```
class С2: . . . # Создание объектов суперклассов
class СЗ: . . .
class C1(С2, СЗ) :
def __init__ (self, who): # Установка name при конструировании
self. name = who # self является либо I1, либо I2
I1 = C1( 'bob' ) # Установка I1.name в 'bob'
I2 = C2( 'sue' ) # Установка I2.name в 'sue'
print(I1.name) # Выводит 'bob'
```
Каждый раз, когда экземпляр генерируется из класса, Python автоматически вызывает метод по имени__ init__ , будь он реализован или унаследован. Как обычно, новый экземпляр передается в аргументе self метода__ init__ , а значения, перечисленные в круглых скобках при обращении к классу, передаются второму и последующим аргументам. Результатом оказывается инициализация экземпляров, когда они создаются, без необходимости в дополнительных вызовах методов.
Метод__ init__ известен как конструктор из-за момента своего запуска. Он является самым часто используемым представителем методов перегрузки операций.Такие методы обычным образом наследуются в деревьях классов и содержат в начале и конце своих имен по два символа подчеркивания, чтобы акцентироватьвнимание на их особенности. Python запускает их автоматически, когда экземпляры, поддерживающие методы, встречаются в соответствующих операциях, и они главным образом выступают в качестве альтернативы применению простых вызовов методов.
Кроме того, они необязательны: если операции опущены, то они не поддерживаются.
При отсутствии__ init__ обращения к классам возвращают пустые экземпляры без их инициализации.
### Полиморфизм и классы
```
class Employee: # Универсальный суперкласс
def computeSalary (self) : . . . # Общие или стандартные линии поведения
def giveRaise(self): ...
def promote(self): ...
def retire(self): ...
```
После написания кода общего поведения вы можете специализировать его для каждого индивидуального типа сотрудника, отражая его отличия от нормы.
```
class Engineer (Employee) : # Специализированный подкласс
def computeSalary(self) : ... # Что-то специальное
```
Из-за того, что версия computeSalary находится ниже в дереве классов, она заместит (переопределит) универсальную версию в Employee. Затем вы создаете экземпляры разновидностей классов сотрудников, к которым принадлежат реальные сотрудники, чтобы получить корректное поведение:
```
bob = Employee () # Стандартное поведение
sue = Employee () # Стандартное поведение
tom = Engineer () # Специальный расчет заработной платы
```
В конце концов, эти три объекта экземпляров могут оказаться встроенными в более крупный контейнерный объект (например, список или экземпляр другого класса), который представляет отдел или компанию, воплощая упомянутую в начале главы идею композиции. Когда вы позже запросите заработные платы сотрудников, они будут рассчитываться в соответствии с классами, из которых создавались объекты, благодаря принципам поиска в иерархии наследования:
```
company = [bob, sue, tom] # Составной объект
for emp in company:
print(emp. computeSalary()) # Выполнить версию для данного объекта:
# стандартную или специальную
```
полиморфизм означает, что смысл операции зависит от объекта, с которым она работает. Таким образом, код не должен заботиться о том, чем объект является, а лишь о том, что он делает. Перед вызовом метод computeSalarу ищется в иерархии наследования для каждого объекта.
Совокупный эффект заключается в том, что мы автоматически запускаем корректную версию для обрабатываемого объекта.
В других приложениях полиморфизм также может использоваться для сокрытия (т.е. инкапсуляции} отличий в интерфейсах.
## Основы написания классов
```
class FirstClass: # Определить объект класса
def setdata(self, value): # Определить методы класса
self.data = value # self - это экземпляр
def display(self):
print(self.data) # self.data: для каждого экземпляра
```
В качестве еще одного способа оценить, насколько динамична эта модель, имейте в виду, что мы можем изменять атрибуты в самом классе, присваивая self в методах, или за пределами класса путем присваивания явному объекту экземпляра:
```
x.data = 'New value' # Можно получать /устанавливать атрибуты
х.display() # И за пределами класса тоже
New value
```
Хотя и менее часто, мы могли бы генерировать совершенно новый атрибут в пространстве имен экземпляра, присваивая значение его имени за пределами функций методов класса:
```
х. anothername = 'spam' # Здесь можно также устанавливать новые атрибуты!
```
Такой оператор присоединит к объекту экземпляра х новый атрибут по имени
anothername, который может применяться или нет любым методом класса. Классы обычно создают все атрибуты экземпляра путем присваивания аргумента self, но они не обязаны поступать так — программы могут извлекать, изменять или создавать атрибуты для любых объектов, ссылками на которые они располагают.