## Общая картина В чем сущность ООП в Python? ООП предназначено для многократного использования кода. В чем отличие между объектом класса и объектом экземпляра? Классы являются своего рода фабриками для создания множества экземпляров. Классы также поддерживают методы перегрузки операций, которые экземпляры наследуют, а любые функции, вложенные внутрь классов, трактуются как методы для обработки экземпляров. ### Поиск в иерархии наследования ![задачи пример](https://whoisdeveloper.ru/static/img/class1.png) На рисунке изображено дерево из пяти объектов, помеченных переменными, которые имеют присоединенные атрибуты, готовые для поиска. Более конкретно дерево связывает вместе три объекта классов (овалы 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, но они не обязаны поступать так — программы могут извлекать, изменять или создавать атрибуты для любых объектов, ссылками на которые они располагают.