На этом занятии мы начнем рассмотрение очень важной темы в ООП
– наследование классов. На первом уроке данного курса я приводил пример с
кофемолкой и говорил, что для создания электрической кофемолки можно взять уже
готовые наработки механической кофемолки, добавить туда мотор, схему и получим
новый прибор. Этот пример как раз описывает общий принцип наследования в ООП:
мы можем взять некий класс и на его основе создать новый, дочерний, изменив и
расширив функционал базового класса.
Причем, в Python 3 любой
создаваемый класс, например:
class Point:
def __init__(self, x = 0, y = 0):
self.__x = x
self.__y = y
автоматически
является дочерним по отношению к базовому object:
И в этом легко
убедиться, вызвав специальную функцию:
print( issubclass(Point, object) )
которая возвращает
True, если класс
является дочерним для класса, указанного вторым параметром.
Теперь давайте
посмотрим как в Python реализуется механизм наследования. Для
начала создадим вспомогательный класс Point для хранения
координат на плоскости:
class Point:
def __init__(self, x = 0, y = 0):
self.x = x
self.y = y
И после него объявим класс для работы с графическим примитивом линией:
class Line:
def __init__(self, sp:Point, ep:Point, color:str = "red", width:int = 1):
self._sp = sp
self._ep = ep
self._color = color
self._width = width
def drawLine(self):
print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self._width}")
Обратите внимание вот на эту нотацию (:Point, :str, :int). Так можно в тексте программы указывать: какого типа параметры мы здесь ожидаем получать. Конечно, это не значит, что другие типы здесь не могут быть переданы (сюда по прежнему можно передать что угодно), это лишь подсказка программисту с чем мы тут собираемся иметь дело – с объектами класса Point.
Создадим экземпляр этого класса:
l = Line( Point(1,2), Point(10,20) )
l.drawLine()
И в консоли увидим не совсем читаемое сообщение. Дело в том, что при вызове self._sp возвращаются не координаты точки, а стандартное сообщение класса Point. Давайте его переопределим, чтобы выводились координаты:
def __str__(self):
return f"({self.__x}, {self.__y})"
Теперь при запуске видим то, что нам было нужно.
Далее, мы подумали и решили добавить класс еще одного примитива – прямоугольника. И мы пишем следующее:
class Rect:
def __init__(self, sp:Point, ep:Point, color:str = "red", width:int = 1):
self._sp = sp
self._ep = ep
self._color = color
self._width = width
def drawRect(self):
print(f"Рисование прямоугольника: {self._sp}, {self._ep}, {self._color}, {self._width}")
И, затем, вызов:
r = Rect( Point(30,40), Point(70,80) )
r.drawRect()
Смотрите, мы
здесь фактически, продублировали текст из класса Line. Отличие только
вот этот метод drawRect, остальное абсолютно такое же. Как
только что-то подобное встречается в тексте программы, это значит, что
программист нарушает общеизвестный принцип DRY (Don’t Repeat Yourself) – не
повторяйся! И как раз это можно поправить с помощью механизма наследования.
Вот этот
дублирующий текст вынесем в отдельный класс:
class Prop:
def __init__(self, sp:Point, ep:Point, color:str = "red", width:int = 1):
self._sp = sp
self._ep = ep
self._color = color
self._width = width
а, классы
примитивов будем наследовать от него:
class Line(Prop):
def drawLine(self):
print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self._width}")
class Rect(Prop):
def drawRect(self):
print(f"Рисование прямоугольника: {self._sp}, {self._ep}, {self._color}, {self._width}")
То есть, мы в
круглых скобках после имени класса указываем базовый класс. В результате,
получаем такую иерархию:
И когда мы
создаем объекты этих примитивов:
l = Line( Point(1,2), Point(10,20) )
r = Rect( Point(30,40), Point(70,80) )
то вот этот self ссылается на
общий класс Line, поэтому все
публичные свойства и методы создаются для дочерних классов Line и Rect.
Вот это нижнее
подчеркивание сигнализирует программисту о режиме доступа protected к этим
атрибутам. То есть, мы оговариваем, что они должны использоваться только внутри
объекта Prop и во всех его
дочерних классах. И здесь ключевое слово оговариваем. Python при этом никак
не ограничивает область их использования. Фактически, это такие же публичные
атрибуты, только с одним нижним подчеркиванием. Если выполнить вот такую
операцию:
то это свойство
будем выведено в консоль без каких-либо ошибок. Но тогда зачем нам писать это
нижнее подчеркивание, если оно не играет никакой роли. Одна роль у этого
префикса все-таки есть: нижнее подчеркивание должно предостерегать программиста
от использования этого свойства вне класса. Впоследствии это может стать
причиной непредвиденных ошибок. Например, изменится версия модуля и такое
свойство может перестать существовать, т.к. никто не предполагал доступа к нему
извне. А вот сеттеры и геттеры будут по-прежнему работать и давать тот же
результат. Так что, к таким атрибутам лучше не обращаться напрямую – это
внутренние, служебные переменные.
Далее, создавая
экземпляры дочерних классов, мы использовали конструктор базового класса Prop. В
действительности, сначала вызывается конструктор в дочерних классах, но так как
мы его явно там не прописывали, то использовался конструктор по умолчанию,
который вызывал конструктор базового класса.
Давайте переопределим
конструктор в дочернем классе Line и убедимся в
этом:
def __init__(self, *args):
print("Переопределенный конструктор Line")
Если теперь
запустить программу, то мы увидим сообщение «Переопределенный конструктор» и,
затем ошибку, связанную с отсутствием выводимых свойств, так как конструктор
базового класса не был вызван и локальные свойства не были созданы.
Конструктор
суперкласса можно вызвать, просто обратившись к нему:
Prop.__init__(self, *args)
И в конструкторе
Prop добавим:
print("Конструктор базового класса Prop")
Но делать так –
порочная практика. В случае множественного наследования – это источник
потенциальных ошибок. Почему? Мы об этом позже еще поговорим. А сейчас отметим,
что вместо явного указания имени базового класса, следует вызвать специальную
функцию super, которая в
правильном порядке будет перебирать вышестоящие классы:
О ней мы
подробнее еще поговорим. Теперь, наша программа вновь обрела работоспособность
и мы видим порядок вызовов конструкторов: от дочернего к базовому классу.
Вот так в самом
простом варианте работает механизм наследования. Давайте, теперь немного
усложним программу и посмотрим как наследуются приватные (частные) свойства.
Укажем в суперклассе Prop свойство _width как приватное:
добавим в него метод
def getWidth(self):
return self.__width
При запуске
видим ожидаемую ошибку, т.к. частное свойство __width базового класса
Prop стало
недоступно в дочернем классе Line. Но почему? Ведь self в базовом
классе Prop ссылается на Line. Так и есть, но
создавая приватную переменную в каком-либо определенном классе, она создается с
префиксом этого класса и среди локальных свойств Line мы увидим
свойство
_Prop__width
Выведем их в
косоль:
и вот в списке
локальных атрибутов объекта Line появилось _Prop__width со значением 1.
Причем, обратиться к нему мы можем только из метода getWidth класса Prop:
def drawLine(self):
print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self.getWidth()}")
Вызывая метод getWidth базового класса,
мы сначала ищем его в классе Line, не находим и переходим в базовый класс
Prop. Там он есть,
вызываем его и, обращаясь к частному свойству уже из этого класса, к нему
добавляется префикс _Prop и мы успешно читаем этот локальный
атрибут из экземпляра класса Line.
Если в
конструкторе класса Line после вызова конструктора базового класса создать такое
же частное свойство:
то в экземпляре
класса Line появится запись:
_Line__width
к которому мы
можем обратиться напрямую из этого класса:
def drawLine(self):
print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self.__width}")
Вот так
создаются приватные атрибуты классов. Ко всем остальным можно напрямую
обращаться из любых экземпляров классов.
Задания для самоподготовки
1. Создайте
суперкласс «Персональные компьютеры» и на его основе подклассы: «Настольные ПК»
и «Ноутбуки». В базовом классе определите общие свойства: размер памяти, диска,
модель, CPU. А в
производных классах уникальные свойства:
-
для
настольных: монитор, клавиатура, мышь, их габариты; и метод для вывода этой
информации в консоль;
-
для
ноутбуков: габариты, диагональ экрана; и метод для вывода этой информации в
консоль.
2. Повторите это
задания для суперкласса «Человек» и подклассов «Мужчина» и «Женщина».
Подумайте, какие общие характеристики можно выделить в суперкласс и какие
частные свойства указать в подклассах.