# Простое наследование классов, режим доступа protected
# Общий принцип наследования в ООП: мы можем взять некий класс и на его основе создать новый,
#                                   дочерний, изменив и расширив функционал базового класса.

class Point:                          # Когда создаем класс Point, то класс Point наследуется от класса object
    def __init__(self, x = 0, y = 0):
        self.__x = x
        self.__y = y

print( issubclass(Point, object) )    # Функция issubclass - которая возвращает True, если класс является дочерним для класса, указанного вторым параметром
#              дочерний к object = True
#
#               Базовый  класс
#                  ^       ^
#               Дочерний класс  -  создается на основе базового
#
#  Python от версии 3+
#
#               object класс
#                  ^     ^
#               Point  класс  -  любой класс который создается, не явно наследуется от базового класса object
#


# Реализация наследования:
class Point1:                          # Когда создаем класс Point1, то класс Point наследуется от класса object
    def __init__(self, x = 0, y = 0):
        self.__x = x
        self.__y = y

    def __str__(self):                      # переопределим класс Point1 чтоб выводились его координаты
        return f"({self.__x}, {self.__y})"  # переопределим стандартный метод __str__ и скажем, чтоб он нам возвращал координаты

class Line:
    def __init__(self, sp:Point1, ep:Point1, color:str = "red", width:int = 1): # Параметры в конструкторе на основе которых
        # определим соответствующие локальные св-ва _sp, _ep, _color, _width данного класса. sp:Point - это нотация (ожидаем в качестве первого параметра будет передан объект Point, т.е. точка на плоскости)
        # можем передавать любые типы данных, это подсказка для developers, что ожидаем получить
        self._sp = sp
        self._ep = ep
        self._color = color
        self._width = width

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

class Rect:     # создадим класс примитива прямоугольника
    def __init__(self, sp:Point1, ep:Point1, color:str = "red", width:int = 1):
        self._sp = sp
        self._ep = ep
        self._color = color
        self._width = width

    def drawRect(
            self):  # в классе Line определим метод drowLine, который будет выводить (все локальные параметры атрибуты выводить в консоль)
        print(f"Рисование прямоугольника: {self._sp}, {self._ep}, {self._color}, {self._width}")

l = Line( Point1(1,2), Point1(10,20) )  # Создадим экземпляр l класса Line, и передадим экземпляру l класса Line два параметра
l.drawLine()                            # Вызовем метод drawLine : Нечитаемое сообщение : Рисование линии: <__main__.Point object at 0x00000232ADED5FD0>, <__main__.Point object at 0x00000232ADED5970>, red, 1
                                        # Причина: когда обращаемся к св-ву _sp, в print {self._sp} --> возвращается стандартное сообщение класса Point1, переопределим класс,
                                        # переопределим класс Point1 чтоб выводились его координаты

r = Rect( Point1(30,40), Point1(70,80) )    # Создадим экземпляр l класса Line, и передадим экземпляру l класса Line два параметра
r.drawRect()



# Don`t repeat yourself - DRY  - механизм наследования
class Point2:                          # Когда создаем класс Point1, то класс Point наследуется от класса object
    def __init__(self, x = 0, y = 0):
        self.__x = x
        self.__y = y

    def __str__(self):                      # переопределим класс Point1 чтоб выводились его координаты
        return f"({self.__x}, {self.__y})"  # переопределим стандартный метод __str__ и скажем, чтоб он нам возвращал координаты

class PropDRY:                         # создадим отдельный класс для дублирующего текста
    def __init__(self, sp:Point2, ep:Point2, color:str = "red", width:int = 1): # Параметры в конструкторе на основе которых
        # определим соответствующие локальные св-ва _sp, _ep, _color, _width отдельного класса для дублирующего текста. sp:Point - это нотация (ожидаем в качестве первого параметра будет передан объект Point, т.е. точка на плоскости)
        # можем передавать любые типы данных, это подсказка для developers, что ожидаем получить
        self._sp = sp
        self._ep = ep
        self._color = color
        self._width = width

class Line2(PropDRY):     # классы графических примитивов будем наследовать от базового класса PropDRY
    def drawLine2(self):  # в классе Line определим метод drowLine, который будет выводить (все локальные параметры атрибуты выводить в консоль)
        print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self._width}")

class Rect2(PropDRY):     # классы графических примитивов будем наследовать от базового класса PropDRY
    def drawRect2(
            self):  # в классе Line определим метод drowLine, который будет выводить (все локальные параметры атрибуты выводить в консоль)
        print(f"Рисование прямоугольника: {self._sp}, {self._ep}, {self._color}, {self._width}")


l2 = Line2( Point2(4,4), Point2(5,5) )  # Создадим экземпляр l класса Line, и передадим экземпляру l класса Line два параметра
l2.drawLine2()                          # Вызовем метод drawLine : Нечитаемое сообщение : Рисование линии: <__main__.Point object at 0x00000232ADED5FD0>, <__main__.Point object at 0x00000232ADED5970>, red, 1
                                        # Причина: когда обращаемся к св-ву _sp, в print {self._sp} --> возвращается стандартное сообщение класса Point1, переопределим класс,
                                        # переопределим класс Point1 чтоб выводились его координаты

r2 = Rect2( Point2(7,7), Point2(8,8) )  # Создадим экземпляр l класса Line, и передадим экземпляру l класса Line два параметра
print(r2._width)                        # Публичные атрибуты только с нижним подчеркиванием - напрямую обратимся к локальному свойству - в консоли увидим его значение 1 - извне смогли обратиться
                                        # Зачем тогда писать _width ? Чтобы предостерегать програмиста от использования св-ва вне класса, например изменится версия модуля и
                                        # и такое св-во перестанет существовать, т.к. никто не предпологал доступа к нему из вне. Сеттеры set и геттеры get будут по прежнему работать и давать результат
                                        # print(r2._width) - к такому атрибуту с _ нижним подчеркиванием лучше не обращаться напрямую - это внутренняя служебная переменная.
r2.drawRect2()


#
#               object
#                  ^
#               PropDRY             класс PropDRY наследуется от класса object
#             ^        ^
#           Line2    Rect2          дочерние классы Line2, Rect2 наследуются от класса PropDRY

# когда создаем экземпляр l2 класса Line2( Point2(4,4), Point2(5,5) ), то у нас вызывается конструктор __init__ базового класса PropDRY и
# первый параметр self будет ссылаться на экземпляр l2 класса Line2, в результате чего создаются локальные св-ва sp, ep , color, width
# с соответствующим содержимым внутри класса Line2
# - тоже самое происходит при создании экземпляра r2 класса Rect2
# когда создаем экземпляр r2 класса Rect2( Point2(7,7), Point2(8,8) ), то у нас вызывается конструктор __init__ базового класса PropDRY и
# первый параметр self будет ссылаться на экземпляр r2 класса Rect2, в результате чего создаются локальные св-ва sp, ep , color, width
# с соответствующим содержимым внутри класса Rect2



# Далее, создавая экземпляры дочерних классов Line2( Point2(4,4), Point2(5,5) ), мы использовали конструктор базового класса PropDRY __init__.
# В действительности, сначала вызывается конструктор в дочерних классах Line2, Rect2, а уже потом конструктор базового класса,
# но так как мы его явно в дочерних классах Line2, Rect2 не прописывали никакого конструктора, то в дочерних классах Line2, Rect2 использовался конструктор по умолчанию,
# который вызывал автоматически конструктор базового класса PropDRY.
# Давайте в качестве примера переопределим этот конструктор в дочернем классе Line2 и убедимся в этом:

class Point3:                          # Когда создаем класс Point1, то класс Point наследуется от класса object
    def __init__(self, x = 0, y = 0):
        self.__x = x
        self.__y = y

    def __str__(self):                      # переопределим класс Point1 чтоб выводились его координаты
        return f"({self.__x}, {self.__y})"  # переопределим стандартный метод __str__ и скажем, чтоб он нам возвращал координаты

class PropDRY1:                         # создадим отдельный класс для дублирующего текста
    def __init__(self, sp:Point3, ep:Point3, color:str = "red", width:int = 1): # Параметры в конструкторе на основе которых
        # определим соответствующие локальные св-ва _sp, _ep, _color, _width отдельного класса для дублирующего текста. sp:Point - это нотация (ожидаем в качестве первого параметра будет передан объект Point, т.е. точка на плоскости)
        # можем передавать любые типы данных, это подсказка для developers, что ожидаем получить
        self._sp = sp
        self._ep = ep
        self._color = color
        self._width = width
        print("Конструктор базового класса PropDRY1")

class Line3(PropDRY1):            # переопределим автоматический конструктор __init__ класса Line3  от базового класса PropDRY1
    def __init__(self,  *args):   # переопределим автоматический конструктор __init__ класса Line3  от базового класса PropDRY1 # Error - отсутствие выводимых свойств {self._sp}, {self._ep}, {self._color}, {self._width}
                                  # т.к. конструктор __init__ базового класса PropDRY1 не был вызван и соответствующие локальные св-ва не были созданы _sp, _ep, _color, ._width
        print( "Переопределенный конструктор Line3" )
        PropDRY1.__init__(self, *args) # в явном виде вызовем конструктор базового класса (имя базового класса PropDRY1. через точку соответствующий метод __init__
                                       # НЕЛЬЗЯ вызывать конструктор базового класса PropDRY1 непосредственно указывая этот класс, так делать нельзя, это – порочная практика.
                                       # В случае множественного наследования – это источник потенциальных ошибок. Почему? Мы об этом позже еще поговорим.
                                       # Правильно использовать ф-цию super, которая в правильном порядке будет перебирать вышестоящие классы.
                                       # вместо явного указания имени базового класса PropDRY1, следует вызвать специальную функцию super,
                                       # которая в правильном порядке будет перебирать вышестоящие классы

    def drawLine3(self):  # в классе Line определим метод drowLine, который будет выводить (все локальные параметры атрибуты выводить в консоль)
        print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self._width}")

class Rect3(PropDRY1):     # классы графических примитивов будем наследовать от базового класса PropDRY
    def drawRect3(self):   # в классе Line определим метод drowLine, который будет выводить (все локальные параметры атрибуты выводить в консоль)
        print(f"Рисование прямоугольника: {self._sp}, {self._ep}, {self._color}, {self._width}")


l3 = Line3( Point3(44,44), Point3(55,55) )  # Создадим экземпляр l класса Line, и передадим экземпляру l класса Line два параметра
#l3.drawLine3()                          # Вызовем метод drawLine : Нечитаемое сообщение : Рисование линии: <__main__.Point object at 0x00000232ADED5FD0>, <__main__.Point object at 0x00000232ADED5970>, red, 1
                                        # Причина: когда обращаемся к св-ву _sp, в print {self._sp} --> возвращается стандартное сообщение класса Point1, переопределим класс,
                                        # переопределим класс Point1 чтоб выводились его координаты

#r3 = Rect3( Point3(77,77), Point3(88,88) )  # Создадим экземпляр l класса Line, и передадим экземпляру l класса Line два параметра
#r3.drawRect3()


#  Правильно использовать ф-цию super().__init__(*args), которая в правильном порядке будет перебирать вышестоящие классы.
#  вместо явного указания имени базового класса PropDRY2, следует вызвать специальную функцию super,
#  которая в правильном порядке будет перебирать вышестоящие классы.
#  Наследование приватных частных свойств - в базовом классе PropDRY2 укажем частное св-во __width
#  Метод getWidth(self): - чтоб могли получать частное свойство
#

class Point4:                          # Когда создаем класс Point1, то класс Point наследуется от класса object
    def __init__(self, x = 0, y = 0):
        self.__x = x
        self.__y = y

    def __str__(self):                      # переопределим класс Point1 чтоб выводились его координаты
        return f"({self.__x}, {self.__y})"  # переопределим стандартный метод __str__ и скажем, чтоб он нам возвращал координаты

class PropDRY2:                         # создадим отдельный класс для дублирующего текста
    def __init__(self, sp:Point4, ep:Point4, color:str = "red", width:int = 1): # Параметры в конструкторе на основе которых
        # определим соответствующие локальные св-ва _sp, _ep, _color, _width отдельного класса для дублирующего текста. sp:Point - это нотация (ожидаем в качестве первого параметра будет передан объект Point, т.е. точка на плоскости)
        # можем передавать любые типы данных, это подсказка для developers, что ожидаем получить
        self._sp = sp
        self._ep = ep
        self._color = color
        self.__width = width                # указал частное св-во width
        print("Конструктор базового класса PropDRY2")

    def getWidth(self):                     # метод getWidth  для получения частного св-ва __width
        return self.__width                 # чтоб могли получать частное (приватное) свойство __width

class Line4(PropDRY2):            # переопределим автоматический конструктор __init__ класса Line3  от базового класса PropDRY1
    def __init__(self,  *args):   # переопределим автоматический конструктор __init__ класса Line3  от базового класса PropDRY1 # Error - отсутствие выводимых свойств {self._sp}, {self._ep}, {self._color}, {self._width}
                                  # т.к. конструктор __init__ базового класса PropDRY1 не был вызван и соответствующие локальные св-ва не были созданы _sp, _ep, _color, ._width
        print( "Переопределенный конструктор Line4" )
        super().__init__(*args)        # Правильно использовать ф-цию super, которая в правильном порядке будет перебирать вышестоящие классы.
                                       # вместо явного указания имени базового класса PropDRY1, следует вызвать специальную функцию super,
                                       # которая в правильном порядке будет перебирать вышестоящие классы

                                       # # Если в конструкторе класса Line4 после вызова конструктора базового класса создать такое же частное свойство: self.__width = 5
        self.__width = 5               # то у нас появится еще одно свойство, но уже с префиксом класса _Line4__width

    def drawLine4(self):  # в классе Line определим метод drowLine, который будет выводить (все локальные параметры атрибуты выводить в консоль)
        print(f"Рисование линии: {self._sp}, {self._ep}, {self._color}, {self.getWidth()}")
                                                                               #  частное (приватное) свойство __width
                                                                               #  Обратиться к частному свойству __width можем через метод getWidth (пропишем в дочернем классе Line4 getWidth)

                                                                               #  Если запишем вместо getWidth() частную переменную __width при запуске поллучим 5, потому что обращаемся к ней из класса Line4
                                                                               #  Поэтому префикс дописуется классса _Line__width

class Rect4(PropDRY2):     # классы графических примитивов будем наследовать от базового класса PropDRY
    def drawRect3(self):   # в классе Line определим метод drowLine4, который будет выводить (все локальные параметры атрибуты выводить в консоль)
        print(f"Рисование прямоугольника: {self._sp}, {self._ep}, {self._color}, {self.__width}")
                                                                               #  частное (приватное) свойство __width



l4 = Line4( Point4(66,66), Point4(77,77) )
l4.drawLine4()                                     # AttributeError: 'Line4' object has no attribute '_Line4__width' - частное свойство __width базового класса PropDRY стало недоступно в дочернем классе Line4.
                                                    # не можем получить доступ к приватной переменной __width почему?
                                                    # ведь когда создаем __width self - в базовом клессе PropDRY - self указывает на класс Line4, и значит в классе Line4 мы создаем частную переменную __width, а не в классе PropDRY
                                                    # Когда мы создаем приватную переменную в каком-либо классе, то она создается с префиксом этого класса,
                                                    # и уже мы увидим частное св-во __width с префиксом класса PropDRY2, _PropDRY2__width
print (l4.__dict__)                                 # проверка - в списке появилось частное свойство _PropDRY2__width
                                                    # {'_sp': <__main__.Point4 object at 0x000001DCFDCDCAC0>, '_ep': <__main__.Point4 object at 0x000001DCFDCDCA60>, '_color': 'red', '_PropDRY2__width': 1}
                                                    # т.е. внутри экземпляра l4 класса Line4 у нас появилось св-во, именно поэтому мы не можем к нему обратиться у него не тот префикс, у него префикс класса PropDRY2
                                                    # Обратиться к частному свойству __width можем через метод getWidth (пропишем в дочернем классе Line4 getWidth)




#                 Prop
#              getWidth(self)
#                  ^
#                 Line
#             drawLine(self)
#             ______________
#                 Prop
#             ______________
#                 Line
#        _sp, _ep, _Prop__width, _color
#
# Вызывая метод getWidth() базового класса Prop из экземпляра класса Line, мы сначала обращаемся (ищем) метод getWidth() в классе Line, не находим и переходим в базовый класс Prop.
# В базовом классе Prop есть метод getWidth(), вызываем его и через параметр self, метод getWidth() вызывается и обращается снова к экземпляру класса Line, и ищет
# частную переменную, свойство _Prop__width_, потому что метод getWidth был вызван из класса Prop, поэтому частная переменная будет искаться в таком виде,
# префиксом этого класса _Prop__width_, поэтому таким путем мы можем обратиться к частной переменной.

# Если в конструкторе класса Line4 после вызова конструктора базового класса создать такое же частное свойство: self.__width = 5
# то у нас появится еще одно свойство, но уже с префиксом класса _Line4__width
# {'_sp': <__main__.Point4 object at 0x000002D5A647CAC0>, '_ep': <__main__.Point4 object at 0x000002D5A647CA60>, '_color': 'red', '_PropDRY2__width': 1, '_Line4__width': 5}

# Ко всем остальным атрибутам можно обращаться напрямую из других экземпляров классов