Курс по Python ООП: https://stepik.org/a/116336
Мы продолжаем
изучение темы «наследование». Это занятие я начну с простой, но важной
терминологии. Сморите, если у нас имеется некий базовый класс Geom и мы создаем
дочерний класс Line, в котором дополнительно прописан метод draw(), то это
называется расширением (extended) базового класса:
class Geom:
name = 'Geom'
class Line(Geom):
def draw(self):
print("Рисование линии")
Как правило,
дочерние создаются именно для расширения функциональности базовых классов.
Однако, если в классе Geom также прописать метод draw():
class Geom:
name = 'Geom'
def draw(self):
print("Рисование примитива")
то теперь класс Line лишь переопределяет
(overriding) поведение базового класса, не меняя его принцип функционирования. Поэтому,
когда говорят о расширении, то подразумевают добавление новых атрибутов в
дочерних классах, а при переопределении (обычно методов) – изменение поведения
уже существующего функционала.
Функция super() и делегирование
Давайте теперь
пропишем инициализатор в базовом классе Geom (метод draw уберем):
class Geom:
name = 'Geom'
def __init__(self):
print("инициализатор Geom")
А ниже создадим
экземпляр класса Line:
После запуска
программы увидим ожидаемое поведение – был автоматически вызван инициализатор
базового класса. В действительности, здесь происходит следующая последовательность
вызовов магических методов. Сначала вызывается __call__(), который, в
свою очередь, последовательно вызывает метод __new__() для
создания экземпляра класса, а затем, метод __init__() для его
инициализации. Так вот, все эти методы вызываются из дочернего класса Line. Если
какой-либо из них не находится, то поиск продолжается в родительских классах в
порядке иерархии наследования. Например, метод __new__() в данном
случае будет взят из метакласса type, который неявно вызывается при создании классов (подробнее о метаклассах мы еще будем говорить). А вот метод __init__() мы прописали в классе Geom, поэтому будет
вызван именно он. Причем, параметр self в этом методе
будет ссылаться на созданный объект класса Line. Об этом мы с
вами уже говорили и это следует помнить. Параметр self в методах класса
всегда ссылается на объект, из которого метод был вызван.
Отлично, я думаю
в целом схема вызова методов в момент создания экземпляров классов, понятна. И
в соответствии с ней, если мы определим инициализатор в классе Line, то именно он и
должен вызваться. Давайте это сделаем:
class Line(Geom):
def __init__(self):
print("инициализатор Line")
def draw(self):
print("Рисование линии")
Запустим
программу и теперь видим, что действительно, вызывается именно метод __init__ класса Line. Я перепишу его
со следующими параметрами:
def __init__(self, x1, y1, x2, y2):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
Это координаты
начала и конца линии на плоскости. Соответственно, при создании объектов этого
класса, мы теперь должны передавать аргументы:
Все работает и
никаких проблем у нас нет. Но, давайте теперь добавим еще один класс Rect для
прямоугольников:
class Rect(Geom):
def __init__(self, x1, y1, x2, y2, fill=None):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.fill = fill
def draw(self):
print("Рисование прямоугольника")
Смотрите, у нас
получилось дублирование кода. Это очень нехорошо. Но мы знаем, как это можно
поправить. Давайте общее этих методов вынесем в базовый класс Geom:
class Geom:
name = 'Geom'
def __init__(self, x1, y1, x2, y2):
print(f"инициализатор Geom для {self.__class__}")
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
В дочернем
классе Line уберем
инициализатор, т.к. он полностью повторяется в Geom, а класс Rect запишем в виде:
class Rect(Geom):
def __init__(self, x1, y1, x2, y2, fill=None):
print("инициализатор Rect")
self.fill = fill
def draw(self):
print("Рисование прямоугольника")
Ниже создадим
экземпляры обоих классов:
l = Line(0, 0, 10, 20)
r = Rect(1, 2, 3, 4)
После запуска
увидим следующее:
инициализатор Geom для <class '__main__.Line'>
инициализатор Rect
О чем это
говорит? Для класса Line был вызван инициализатор в базовом
классе Geom, а для класса Rect не вызывался –
только инициализатор самого класса. И это логично, так как метод __init__() был найден
в Rect и дальше
цепочка поиска не продолжалась. Но нам же нужно при создании примитивов также
вызывать инициализатор и базового класса Geom. Как это
сделать? Конечно, мы могли бы явно указать имя базового класса Geom и вызвать через
него магический метод __init__() в инициализаторе класса Rect:
def __init__(self, x1, y1, x2, y2, fill=None):
Geom.__init__(self, x1, y1, x2, y2)
print("инициализатор Rect")
self.fill = fill
Но явно
указывать имена базовых классов не лучшая практика, так как имена и иерархия
наследования могут меняться. Поэтому в Python для обращения к
базовому классу используется специальная функция super():
def __init__(self, x1, y1, x2, y2, fill=None):
super().__init__(x1, y1, x2, y2)
print("инициализатор Rect")
self.fill = fill
Она возвращает
ссылку на, так называемый, объект-посредник, через который происходит вызов
методов базового класса.
Теперь, при
запуске программы мы видим, что был вызван инициализатор сначала класса Geom, а затем, для Rect. Такое
обращение к переопределенным методам базового класса с помощью функции super() называется делегированием.
То есть, мы делегировали вызов инициализатора класса Geom, чтобы он создал
в нашем объекте локальные свойства с координатами углов прямоугольника. Причем,
вызов метода __init__() базового класса лучше делать в первой же
строчке, чтобы он случайно не переопределял какие-либо локальные свойство в
дочернем классе. Например, если в базовом __init__()
дополнительно прописать:
def __init__(self, x1, y1, x2, y2):
print(f"инициализатор Geom для {self.__class__}")
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.fill = 0
А в дочернем его
вызвать в последнюю очередь:
class Rect(Geom):
def __init__(self, x1, y1, x2, y2, fill=None):
print("инициализатор Rect")
self.fill = fill
super().__init__(x1, y1, x2, y2)
то, очевидно,
свойство fill будет неявно переопределено
при вызове __init__() базового
класса:
Подобные причины
и рекомендуют делать вызов инициализатора базового класса в первой же строчке:
class Rect(Geom):
def __init__(self, x1, y1, x2, y2, fill=None):
super().__init__(x1, y1, x2, y2)
print("инициализатор Rect")
self.fill = fill
Теперь у нас нет
проблем с определением локального свойства fill.
Надеюсь, из
этого занятия вы узнали о понятиях расширения классов и переопределения их
методов, разобрались с работой функции super(), а также
делегированием методов, то есть их вызовом из базового класса через функцию super().
Курс по Python ООП: https://stepik.org/a/116336