ООП в Python

Общие понятия

Иерархия наследования (снизу вверх/слева-направо). С2 и С3 — суперклассы. С1 — дочерний класс. I1 и I2 — экземпляр класса С1. Все атрибуты родительских классов, доступны для дочерних классов. Например, для экземпляра I1 доступны атрибуты: name, x,y,z,w.
  • Каждая инструкция class создает новый объект класса.
  • Каждый раз, когда вызывается класс, он создает новый объект экземпляра.
  • Экземпляры автоматически связываются с классами, из которых они были
    созданы.
  • Классы связаны со своими суперклассами, перечисленными в круглых
    скобках в заголовке инструкции class, – порядок следования в списке определяет порядок расположения в дереве.

Для создания дерева классов, изображенного выше, можем использовать следующий программный код:

class C2: ... # Создать объекты классов (овалы)
class C3: ...
class C1(C2, C3): ... # Связанные с суперклассами
I1 = C1() # Создать объекты экземпляров (прямоугольники),
I2 = C1() # связанные со своими классами

Здесь мы построили дерево объектов классов, выполнив три инструкции class
и сконструировав два объекта экземпляров, вызвав класс C1 дважды, как если
бы это была обычная функция. Экземпляры помнят класс, из которого они
были созданы, а класс C1 помнит о своих суперклассах.

Атрибуты, присоединяемые к экземплярам, принадлежат только этим конкретным экземплярам, но атрибуты, присоединяемые к классам, совместно используются всеми подклассами и экземплярами. Позднее мы подробно изучим программный код, выполняющий присоединение атрибутов к этим объектам. Мы увидим, что:

  • Атрибуты обычно присоединяются к классам с помощью инструкций присваивания внутри инструкции class, а не во вложенных инструкциях def,
    определяющих функции.
  • Атрибуты обычно присоединяются к экземплярам с помощью присваивания значений специальному аргументу с именем self, передаваемому функциям внутри классов

Например, классы определяют поведение своих экземпляров с помощью функций, создаваемых инструкциями def внутри инструкции class. Поскольку
такие вложенные инструкции def выполняют присваивание именам внутри
класса, они присоединяются к объектам классов в виде атрибутов и будут унаследованы всеми экземплярами и подклассами:

class C1(C2, C3): # Создать и связать класс C1
    def setname(self, who): # Присвоить: C1.setname
        self.name = who # self – либо I1, либо I2
I1 = C1() # Создать два экземпляра
I2 = C1()
I1.setname(‘bob’) # Записать ‘bob’ в I1.name
I2.setname(‘mel’) # Записать ‘mel’ в I2.name
Print(I1.name) # Выведет ‘bob’

Синтаксис инструкции def в этом контексте – совершенно обычный. С функциональной точки зрения, когда инструкция def появляется внутри инструкции class, как в этом примере, она обычно называется методом и автоматически принимает специальный первый аргумент с именем self, который содержит ссылку на обрабатываемый экземпляр.

Так как классы являются фабриками, способными производить множество экземпляров, их методы обычно используют этот, получаемый автоматически, аргумент self для получения или изменения значений атрибутов конкретного экземпляра, который обрабатывается методом. В предыдущем фрагменте программного кода имя self используется для сохранения имени служащего в конкретном экземпляре.

Подобно простым переменным, атрибуты классов и экземпляров не объявляются заранее, а появляются, когда им впервые выполняется присваивание значений. Когда метод присваивает значение атрибуту с помощью имени self, он тем самым создает атрибут экземпляра, находящегося в нижнем уровне дерева классов (то есть в одном из прямоугольников), потому что имя self автоматически ссылается на обрабатываемый экземпляр.

В настоящий момент класс C1 не присоединяет атрибут name к экземплярам,
пока не будет вызван метод setname. Фактически попытка обратиться к имени
I1.name до вызова I1.setname приведет к появлению сообщения об ошибке, извещающего о неопределенном имени. Если в классе потребуется гарантировать,
что атрибут, такой как name, всегда будет присутствовать в экземплярах, то такой атрибут должен создаваться на этапе создания класса, как показано ниже:

class C1(C2, C3):
    def __init__(self, who): # Создать имя при создании класса
       self.name = who # Self – либо I1, либо I2
I1 = C1(‘bob’) # Записать ‘bob’ в I1.name
I2 = C1(‘mel’) # Записать ‘mel’ в I2.name
Print(I1.name) # Выведет ‘bob’

В этом случае интерпретатор Python автоматически будет вызывать метод
с именем __init__ каждый раз при создании экземпляра класса. Новый экземпляр будет передаваться методу __init__ в виде первого аргумента self, а любые значения, перечисленные в круглых скобках при вызове класса, будут передаваться во втором и последующих за ним аргументах.

Метод __init__ известен как конструктор, так как он запускается на этапе
конструирования экземпляра. Этот метод является типичным представителем
большого класса методов, которые называются методами перегрузки операто-
ров.

Пример использования классов

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

class Employee: # Общий суперкласс
    def computeSalary(self): ... # Общее поведение
    def giveRaise(self): ...
    def promote(self): ...
    def retire(self): ...

Реализовав это общее поведение, можно специализировать его для каждой категории служащих, чтобы отразить отличия разных категорий от стандарта.
То есть можно создать подклассы, которые изменяют лишь ту часть поведения,которая отличает их от типового представления служащего, – остальное поведение будет унаследовано от общего класса. Например, если зарплата инженеров начисляется в соответствии с какими-то особыми правилами (то есть не по почасовому тарифу), в подклассе можно переопределить всего один метод:

class Engineer(Employee): # Специализированный подкласс
    def computeSalary(self): ... # Особенная реализация

Поскольку эта версия computeSalary находится в дереве классов ниже, она будет замещать (переопределять) общую версию метода в классе Employee. Затем
можно создать экземпляры разновидностей классов служащих в соответствии
с принадлежностью имеющихся служащих классам, чтобы обеспечить корректное поведение:

bob = Employee() # Поведение по умолчанию
mel = Engineer() # Особые правила начисления зарплаты

Обратите внимание, что существует возможность создавать экземпляры любых классов в дереве, а не только тех, что находятся внизу, – класс, экземпляр
которого создается, определяет уровень, откуда будет начинаться поиск атрибутов.