|
Переопределение и перегрузка методов, абстрактные методы
Продолжаем тему наследования классов в Python и поговорим о возможности переопределения публичных методов в дочерних классах. Давайте вернемся
к нашему примеру из предыдущего занятия:
class Point:
def __init__(self, x = 0, y = 0):
self.__x = x
self.__y = y
def __str__(self):
return f"({self.__x}, {self.__y})"
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}")
Для изменения
координат графических примитивов добавим в класс Prop метод:
def setCoords(self, sp:Point, ep:Point):
if sp.isDigit() and ep.isDigit():
self._sp = sp
self._ep = ep
else:
print("Координаты должны быть числами")
Который вначале
проверяет: являются ли переданные значения числами или нет. Соответственно, в классе Point мы определим метод:
def isDigit(self):
if (isinstance(self.__x, int) or isinstance(self.__x, float)) and \
(isinstance(self.__y, int) or isinstance(self.__y, float)):
return True
return False
Теперь, мы можем
создать экземпляр класса Line и менять его координаты:
l = Line( Point(1,2), Point(10,20) )
l.drawLine()
l.setCoords( Point(10.1,10), Point(30,30) )
l.drawLine()
Далее, нам
приходит указание свыше устанавливать координаты для объекта Line только
целочисленные. А все остальные примитивы могут принимать как целочисленные, так
и вещественные координаты. Как нам это реализовать? Очевидно, нужно
переопределить метод setCoords в дочернем классе Line. Сделаем это,
запишем его в виде:
def setCoords(self, sp:Point, ep:Point):
if sp.isInt() and ep.isInt():
self._sp = sp
self._ep = ep
else:
print("Координаты должны быть целочисленными")
А в класс Point добавим метод:
def isInt(self):
if isinstance(self.__x, int) and isinstance(self.__y, int):
return True
return False
Теперь, при
запуске программы, мы увидим сообщение, что координаты не целочисленные. То
есть, это сработал переопределенный метод класса Line. А что же метод
базового класса? Существует ли он? Да, и в данном случае, чтобы не дублировать
вот этот код, мы можем его вызвать напрямую из этого класса:
Prop.setCoords(self, sp, ep)
В результате, у
нас получается такая картина использования методов:
То
есть, метод сначала ищется в дочернем классе и если не находится, то поиск
продолжается в базовых. И, так как мы создали аналогичный в дочернем, то он и
был взят, но при необходимости, мы всегда можем обратиться и к методу базового
класса. Здесь всегда следует помнить, что методы в Python ведут себя как статические и обращаясь к ним из экземпляров, мы
берем их непосредственно из классов. И только благодаря параметру self можем «понимать» с каким экземпляром класса работаем.
Но
так переопределять можно только публичные методы, с частными (private) это не работает: они просто будут определяться независимо в
своих классах. Я думаю это вполне очевидно и понятно.
Далее,
в Python методы можно перегружать, то
есть, выполнять разный функционал в зависимости от переданных данных. Например,
можно перегрузить метод setCoords
так, что при передаче только одного аргумента данные будут записываться в
свойство _sp, а при двух он бы работал так как и сейчас. В Python это делается следующим образом. Второй аргумент устанавливается
по умолчанию в None:
def setCoords(self, sp:Point, ep:Point = None):
if ep is None:
if sp.isInt():
self._sp = sp
else:
print("Координата должна быть целочисленной")
else:
if sp.isInt() and ep.isInt():
Prop.setCoords(self, sp, ep)
else:
print("Координаты должны быть целочисленными")
И,
теперь можно вызывать этот метод с одним или двумя аргументами:
l.setCoords( Point(10,10), Point(30,30) )
l.drawLine()
l.setCoords( Point(-10,-10) )
l.drawLine()
Метод
setCoords в данном случае можно улучшить и реализовать через два
вспомогательных метода:
def __setOneCoord(self, sp):
if sp.isInt():
self._sp = sp
else:
print("Координата должна быть целочисленной")
def __setTwoCoords(self, sp, ep):
if sp.isInt() and ep.isInt():
Prop.setCoords(self, sp, ep)
else:
print("Координаты должны быть целочисленными")
def setCoords(self, sp:Point, ep:Point = None):
if ep is None:
self.__setOneCoord(sp)
else:
self.__setTwoCoords(sp, ep)
Так
программа выглядит более читабельной.
Одним
из преимуществ языка Python является возможность перебирать коллекции разнородных объектов и
вызывать у них какой-нибудь единый метод. Например, если мы создаем графический
редактор и пользователь нарисовал множество линий, прямоугольников, эллипсов и
т.п., то мы можем их все сохранить в упорядоченной коллекции и отрисовывать,
вызывая единый метод Draw,
который в них реализуем.
Например,
так:
class Line(Prop):
def draw(self):
print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self._width}")
class Rect(Prop):
def draw(self):
print(f"Рисование прямоугольника: {self._sp}, {self._ep}, {self._color}, {self._width}")
class Ellipse(Prop):
def draw(self):
print(f"Рисование эллипса: {self._sp}, {self._ep}, {self._color}, {self._width}")
figs = []
figs.append( Line(Point(0,0), Point(10,10)) )
figs.append( Line(Point(10,10), Point(20,10)) )
figs.append( Rect(Point(50,50), Point(100,100)) )
figs.append( Ellipse(Point(-10,-10), Point(10,10)) )
for f in figs:
f.draw()
В
других языках программирования, таких как С++ или Java это
реализуется через виртуальные методы. Здесь же весь этот механизм скрыт от нас
и представляется естественным образом. Дополнительно здесь можно поставить
«защиту» на случай, если метод draw не
будет определен в дочернем классе. Для этого в базовом можно прописать такой же
метод, но генерирующий специальное исключение:
def draw(self):
raise NotImplementedError("В дочернем классе должен быть определен метод draw()")
Теперь,
если убрать в каком-либо дочернем классе метод draw, то
он будет вызван из базового класса и возникнет это исключение. Такие методы в Python иногда называют абстрактными,
т.к. для корректной работы они требуют своего обязательного переопределения в
дочерних классах. Хотя, на мой взгляд, это не совсем точное название, здесь
лишь имитация абстракции в реализации метода.
Задания для самоподготовки
1.
Создайте базовый класс «Стол» и дочерние: «Прямоугольные столы» и «Круглые
столы». Через конструктор базового класса передавайте размер поверхности стола:
для прямоугольного – ширина и длина, для круглого – радиус. В дочерних классах реализуйте
метод вычисления площади поверхности стола.
2. Создайте
класс Animal (животное) и разные производные от него подклассы: Fox, Bird, Cat, Dog и т.п. Реализуйте у них
общий метод say(), который бы возвращал звук, издаваемый этим животным. Создайте
кортеж из нескольких этих экземпляров классов, переберите их в цикле и выведите
в консоль их звуки (вызовите метод say()).
|