# Переопределение и перегрузка методов, абстрактные методы.
# Рассмотрим возможность переопределения публичных методов в дочерних классах.
# Рассмотрим пример из предыдущего занятия - уже определены три класса


class Point:                                               # Вспомогательный класс Point для определения точки в двумерном пространстве
    def __init__(self, x=0, y=0):                          # с двумя координатами x y
        self.__x = x                                       # Координаты сохранены внутри класса Point
        self.__y = y

    def __str__(self):
        return f"({self.__x}, {self.__y})"

                                                           # Вводная - устанавливать координаты для объекта Line только целочисленные, а все остальные примитивы кот. могут быть пусть принимают координаты и вещественные и целочисленные.
                                                           # Реализация - переопределить метод setCoords в дочернем классе Line, чтоб применялся только для линий, для объектов Line
    def isDigit(self):                                     # Метод isDigit() реализуем в спомогательном классе Point
                                                           # Проверяем координаты x, y БЫЛИ ЦЕЛОЧИСЛЕННЫМИ ИЛИ ВЕЩЕСТВЕННЫМИ - если это так - возвращается True, иначе False
        if (isinstance(self.__x, int) or isinstance(self.__x, float)) and \
                (isinstance(self.__y, int) or isinstance(self.__y, float)):
            return True
        return False

    def isInt(self):                                      # Добавляем метод isInt() в класс Point
        if isinstance(self.__x, int) and isinstance(self.__y, int):   # Проверяем переменная х, у должна быть целочисленная
            return True
        return False





class Prop:                                                # Класс Prop является базовым по отношению к классу Line, класс Prop задает локальные свойства
                                                           # sp, ep, color, width - дочернего класса 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 setCoords(self, sp, ep):                            # Где нам следует определить seetter? - в базовом классе - чтобы он был применен ко всем графическим примитивам.
        if sp.isDigit() and ep.isDigit():                   # Метод isDigit() реализуем в спомогательном классе Point
             self._sp = sp
             self._ep = ep
        else:
             print("Координаты должны быть числами")

    # Абстрактные методы - требуют обязательного переопределения в дочерних классах
    def draw(self):
        raise NotImplementedError("В дочернем классе должен быть определен метод draw()")


class Line(Prop):
    def drawLine(self):                                     # В классе Line реализован метод drawLine, который выводит в консоль сообщение
        print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self._width}")


    def __setOneCoord(self, sp):                            # Оптимизируем код, метод setCoords через два вспомогательных метода. Сделаем частным - принимает один аргумент
       if sp.isInt():                                       # проверяем если первый аргумент является целочисленным,
          self._sp = sp                                     # устанавливаем локальное св-во sp равное sp
       else:                                                # Иначе выводим сообщение, что координаты должны быть целочисленными
          print("Координата должна быть целочисленной")

    def __setTwoCoords(self, sp, ep):                       # Второй частный метод с двумя аргументами
        if sp.isInt() and ep.isInt():                       # Проверяем, если координата sp содержит только целые числа, и ep содержит только целые числа. Добавляем метод isInt() в класс Point
            Prop.setCoords(self, sp, ep)                    # Т.к. метод setCoords() базового класса Prop продолжает существовать, чтоб не дублировать строчки укажем его
            #   self._sp = sp                               # Меняем локальные св-ва нашего экземпляра
            #   self._ep = ep
        else:                                               # Иначе выводим сообщение, что координаты должны быть целочисленными
            print("Координаты должны быть целочисленными")


                                                            # Вводная - устанавливать координаты для объекта Line только целочисленные, а все остальные примитивы кот. могут быть пусть принимают координаты и вещественные и целочисленные.
                                                            # Реализация - переопределить метод setCoords в дочернем классе Line, чтоб применялся только для линий, для объектов Line

    def setCoords(self, sp: Point, ep: Point = None):       # аргументы sp, ep.  Методы можно перегружать, то есть, выполнять разный функционал в зависимости от переданных данных.
                                                            # можно перегрузить метод setCoords так, что при передаче только одного аргумента данные будут записываться в свойство _sp, а при двух он бы работал так как и сейчас
                                                            # Второй аргумент устанавливается по умолчанию в None:
        if ep is None:                                      # проверка если второй аргумент None, значит мы должны работать только с первым аргументом,
            self.__setOneCoord(sp)                          # вызовем наш первый частный метод с одним аргументом
# Optimize   if sp.isInt():                                  # проверяем если первый аргумент является целочисленным,
#                self._sp = sp                               # устанавливаем локальное св-во sp равное sp
#            else:                                           # Иначе выводим сообщение, что координаты должны быть целочисленными
#                print("Координата должна быть целочисленной")

        else:                                               # Если наша вторая переменная не None, значит выполняем все остальные строчки
            self.__setTwoCoords(sp, ep)                     # вызовем наш второй частный метод с двумя аргументами.

# Optimize  if sp.isInt() and ep.isInt():                       # Проверяем, если координата sp содержит только целые числа, и ep содержит только целые числа. Добавляем метод isInt() в класс Point
#                Prop.setCoords(self, sp, ep)                    # Т.к. метод setCoords() базового класса Prop продолжает существовать, чтоб не дублировать строчки укажем его
#                #   self._sp = sp                                    # Меняем локальные св-ва нашего экземпляра
#                #   self._ep = ep
#            else:                                               # Иначе выводим сообщение, что координаты должны быть целочисленными
#                print("Координаты должны быть целочисленными")


l = Line( Point(1,2), Point(10,20) )                        # Создаем экземпляр класса Line
l.drawLine()                                                # Вызываем метод .drawLine() к экземпляру класса l

# В наших классах нет метода, который бы позволял менять линии, после того как она задана,
# добавим функционал с помощью setter - сеттера
l.setCoords( Point(10, 20), Point(100, 200) )               # Передадим две точки с другими координтами
l.drawLine()                                                # Вызываем метод .drawLine() к экземпляру класса l
                                                            # Где нам следует определить seetter? - в базовом классе - чтобы он был применен ко всем графическим примитивам.
# Point(10.1, 20)                                           # Сработала строчка, ("Координаты должны быть целочисленными") - значит мы вызвали метод setCoords из класса Line, а не из базового класса Prop, т.к. такого сообщения просто нет
l.setCoords( Point(-10, -20) )                              # Метод setCoords оказался перегруженным, его можно вызывать и с двумя аргументами, и с одним аргументом.
l.drawLine()



# То есть, метод сначала ищется в дочернем классе и если не находится, то поиск продолжается в базовых.
# И, так как мы создали аналогичный в дочернем, то он и был взят, но при необходимости, мы всегда можем обратиться и к методу базового класса.
# Здесь всегда следует помнить, что методы в Python ведут себя как статические и обращаясь к ним из экземпляров, мы берем их непосредственно из классов.
# И только благодаря параметру self можем «понимать» с каким экземпляром класса работаем.
# Но так переопределять можно только публичные методы, с частными (private) это не работает: они просто будут определяться независимо в своих классах.
# Я думаю это вполне очевидно и понятно.

print(" ~~ Абстрактные методы ~~ ")

# Одним из преимуществ языка Python является возможность перебирать коллекции разнородных объектов и вызывать у них какой-нибудь единый метод.
# Например, если мы создаем графический редактор и пользователь нарисовал множество линий,
# прямоугольников, эллипсов и т.п., то мы можем их все сохранить в упорядоченной коллекции
# и отрисовывать, вызывая единый метод draw(), который в них реализуем.
class Line1(Prop):
#    pass                                                # проверка, чтоб метод draw() точно был переопределен в дочернем классе
     def draw(self):                                     # В классе Line1 реализован метод draw(), который выводит в консоль сообщение
         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 = []
figs.append(Line1(Point(0, 0), Point(10, 10)))               # С помощью метода append добавляем соответствующие экземпляры классов и переберем их с помощью цикла for
figs.append(Line1(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:                                              # переберем экземпляры классов с помощью цикла for
    f.draw()                                                # для каждого объекта вызываем метод draw(), будет выводить в консоль соответствующие сообщения
                                                            # у нас в каждом классе реализован метод draw(), и мы этот метод вызываем, когда перебираем объекты
# Сделаем защиту метода draw() чтобы он был обязательно реализован в дочерних классах
# Все наши три графических примитива Line, Rect, Ellipse наследуются от класса Prop,
# и пропишем в классе Prop метод draw(), который будет вызывать исключение raise NotImplementedError(),
# и если в классе Line, Rect, Ellipse забудем реализовать метод draw(), то будет вызван метод базового класса draw() - raise NotImplementedError() - возникнет исключение.
# Таким образом мы создаем защиту, чтоб этот метод draw() точно был переопределен в дочернем классе

# Абстрактные методы - требуют обязательного переопределения в дочерних классах


