# Инкапсуляция - режимы доступа - public, private, protected. Геттеры и сеттеры
# Ограничение доступа к данным и методам класса из вне - основа механизма инкапсуляции
# Специальные методы: геттеры и сеттеры - обращение к закрытым (приватным) свойствам через сеттеры и геттеры.

# Инкапсуляция в Python
# класс Point - представление точки на плоскости
class Point:
    def __init__(self, x = 0, y = 0):       # объявлен один конструктор
        self.x = x; self.y = y

pt = Point(1, 2)                            # вызвали конструктор, когда создали экземпляр классa pt
                                            # проверили работоспособность программы - Process finished with exit code 0
print( pt.x, pt.y )                         # к атрибутам x, y  к экземпляру класса pt обратимся напрямую и выведем в консоль
pt.x = 100                                  # чтобы програмист не мог изменять атрибуты x, y  экземпляра класса pt ИЗ ВНЕ - их следует делать закрытыми
pt.y = "abc"                                # чтобы програмист не мог изменять атрибуты x, y  экземпляра класса pt ИЗ ВНЕ - их следует делать закрытыми
print( pt.x, pt.y )                         # выведем в консоль измененные атрибуты x, y  экземпляра класса pt

# В Python возможны следующие варианты доступа к данным:
#  ●  <имя атрибута> (без одного или двух подчеркиваний вначале) – публичное свойство (public);
#  ●  _<имя атрибута> (с одним подчеркиванием) – режим доступа protected (можно обращаться только внутри класса и во всех его дочерних классах)
#  ●  __<имя атрибута> (с двумя подчеркиваниями) – режим доступа private (можно обращаться только внутри класса).




# set - сеттеры, get - геттеры
# Создадим в конструкторе два private атрибута (можно обращаться только внутри класса) ●  __<имя атрибута> (с двумя подчеркиваниями)
class Point1:
    def __init__(self, x = 0, y = 0):       # объявлен один конструктор
        self.__x = x; self.__y = y          # два private атрибута

# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый setCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def setCorrds(self, x, y):              # set - сеттер - означает установить, задать, координату Corrds, два параметра x, y
        self.__x = x                        # внутри метода setCorrds через два параметра мы можем обращаться к этим закрытым св-вам
        self.__y = y
# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый getCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def getCorrds(self):                    # get - геттер
        return self.__x, self.__y           # будем возвращать кортеж этих значений

pt = Point1(3, 4)                           # вызвали конструктор, когда создали экземпляр классa pt
                                            # проверили работоспособность программы - Process finished with exit code 0
print( pt.getCorrds() )                     # вызов метода getCorrds - возвращает кортеж этих значений

pt.setCorrds(30, 40)                        # вызов метода setCorrds с двумя аргументами
print( pt.getCorrds())                      # вызов метода getCorrds - возвращает кортеж этих значений
                                            # с помощью публичных методов setCorrds и getCorrds мы можем задавать и выводить (получать)
                                            # соответствующие локальные св-ва экземпляра класса pt



# set get - назначение не только передавать значения между приватными атрибутами класса, но и проверять их корректность
# например в нашем случае координата - это числа
class Point2:
    def __init__(self, x = 0, y = 0):       # объявлен один конструктор
        self.__x = x; self.__y = y          # два private атрибута

# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый setCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def setCorrds(self, x, y):              # set - сеттер - означает установить, задать, координату Corrds, два параметра x, y
        if (isinstance(x, int) or isinstance(x, float)) and \
                (isinstance(y, int) or isinstance(y, float)):  # прежде чем обнавлять значение переменных следует проверить типы их данных если x, y - числа, тогда их можно туда записать
                                            # с помощью ф-ции isinstance проверяем, что x является типом int или float и тоже самое для y
            self.__x = x
            self.__y = y
        else:
            print("Координаты должны быть числами")

# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый getCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def getCorrds(self):                    # get - геттер
        return self.__x, self.__y           # будем возвращать кортеж этих значений

pt = Point2(5, 6)                           # вызвали конструктор, когда создали экземпляр классa pt
                                            # проверили работоспособность программы - Process finished with exit code 0
print( pt.getCorrds() )                     # вызов метода getCorrds - возвращает кортеж этих значений

pt.setCorrds("50", 60)                      # вызов метода setCorrds с аргументом строкой
print( pt.getCorrds())                      # значения локальных свойств 5, 6 не были изменены на 50, 60




# set get - назначение не только передавать значения между приватными атрибутами класса, но и проверять их корректность
# Проверки корректности данных, часто реализуют в виде вспомогательных методов, которые доступны только внутри класса, то есть, их делают приватными (закрытыми)
# Приватный метод объявляется также как и приватная переменная – двумя подчеркиваниями __checkValue
class Point3:
    def __init__(self, x = 0, y = 0):       # объявлен один конструктор
        self.__x = x; self.__y = y          # два private атрибута

    def __checkValue (x):                   # объявлен приватный метод __checkValue внутри класса Point, который будет проверять наше значение x, является ли оно числом или не является
                                            # self - не пишется, т.к. сам метод не будет обращаться к какой-либо локальной переменной экземпляра нашего класса
        if isinstance(x, int) or isinstance(x, float): # с помощью ф-ции isinstance проверяем, что x является типом int или float и тоже самое для y
            return True                     # если это так возвращаем True
        return False                        # иначе возвращено False


# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый setCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def setCorrds(self, x, y):              # set - сеттер - означает установить, задать, координату Corrds, два параметра x, y
        if Point3.__checkValue(x) and Point3.__checkValue(y):  # чтобы обратиться к методу __checkValue у которого нет параметра self,
                                                               # мы должны записать его через имя класса Point3.__checkValue(x)
                                                               # имя класса Point3. приватный метод __checkValue()  (х) передаем ту переменную которую проверяем
            self.__x = x
            self.__y = y
        else:
            print("Координаты должны быть числами")

# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый getCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def getCorrds(self):                    # get - геттер
        return self.__x, self.__y           # будем возвращать кортеж этих значений

pt = Point3(7, 8)                           # вызвали конструктор, когда создали экземпляр классa pt
                                            # проверили работоспособность программы - Process finished with exit code 0
print( pt.getCorrds() )                     # вызов метода getCorrds - возвращает кортеж этих значений

pt.setCorrds(70, "80")                      # вызов метода setCorrds с аргументом строкой
print( pt.getCorrds())                      # значения локальных свойств 5, 6 не были изменены на 50, 60

# Обратимся из ВНЕ к закрытому методу __checkValue и к закрытым атрибутам __x __y
# _Имя класса__имя переменной
# _Имя класса__имя метода
pt._Point3__x = 100                         # изменим приватную переменную __x, так
print("Изменим приватную переменную __x закрытого атрибута x: ", pt._Point3__x )
                                            # обратимся к локальному св-ву x
Point3._Point3__checkValue(4)               # обращение к методу, вызовем метод
#pt._Point3__x                              # __x - увидел двойное подчеркивание - работай с атрибутом __x через set get - сеттеры и геттеры


# Мы можем осуществлять дополнительный контроль изменения атрибутов, путем перегрузки следующих методов
# ◉  __setattr(self, key, value)__ – автоматически вызывается при изменении свойства key класса;
# ◉  __getattribute__(self, item) – автоматически вызывается при получении свойства класса с именем item;
# ◉  __getattr__(self, item) – автоматически вызывается при получении несуществующего свойства item класса;
# ◉  __delattr__(self, item) – автоматически вызывается при удалении свойства item (не важно: существует оно или нет).

class Point4:
    WIDTH = 5
    # Зададим в классе Point4 константу WIDTH = 5 со значением 5, и сделаем так, чтоб извне не могли менять


    def __init__(self, x = 0, y = 0):       # объявлен один конструктор
        self.__x = x; self.__y = y          # два private атрибута

    def __checkValue (x):                   # объявлен приватный метод __checkValue внутри класса Point, который будет проверять наше значение x, является ли оно числом или не является
                                            # self - не пишется, т.к. сам метод не будет обращаться к какой-либо локальной переменной экземпляра нашего класса
        if isinstance(x, int) or isinstance(x, float): # с помощью ф-ции isinstance проверяем, что x является типом int или float и тоже самое для y
            return True                     # если это так возвращаем True
        return False                        # иначе возвращено False


# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый setCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def setCorrds(self, x, y):              # set - сеттер - означает установить, задать, координату Corrds, два параметра x, y
        if Point4.__checkValue(x) and Point4.__checkValue(y):  # чтобы обратиться к методу __checkValue у которого нет параметра self,
                                                               # мы должны записать его через имя класса Point3.__checkValue(x)
                                                               # имя класса Point3. приватный метод __checkValue()  (х) передаем ту переменную которую проверяем
            self.__x = x
            self.__y = y
        else:
            print("Координаты должны быть числами")

# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый getCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def getCorrds(self):                    # get - геттер
        return self.__x, self.__y           # будем возвращать кортеж этих значений

# Перегрузим метод __getattribute__ следующим образом -  автоматически вызывается при получении свойства класса с именем item
    def __getattribute__(self, item):
        if item == "_Point4__x":             # если св-во item соответствует частной переменной x
            return("Private attribute - частная переменная")
                                            # тогда возвращаем значение - частная переменная
        else:
            return object.__getattribute__(self, item)
                                            # иначе будет вызван getattribute для базового класса object
                                            # чтоб могли работать со всеми атрибутами данного класса

# Перегрузим метод __setattr__ следующим образом - автоматически вызывается при изменении свойства key класса
    def __setattr__(self, key, value):
        if key == "WIDTH":                  # проверяем, если происходит изменение нашего св-ва WIDTH, то мы
            raise AttributeError            # генерируем исключение -  AttributeError
        else:                               # иначе
            self.__dict__[key] = value      # для всех остальных св-в мы можем менять их локальные св-ва через словарь (коллекцию) __dict__
            #self.__x = value               # метод __setattr__ будет по рекурсии запускаться снова и снова,
                                            # пока не произойдет рекурсивное исключение RecursionError
                                            # запись self вызывает метод __setattr__

# Перегрузим метод __getattr__ следующим образом -  автоматически вызывается при получении несуществующего свойства item класса
    def __getattr__(self, item):
        print("__getattr__: "+item)
# Перегрузим метод __delattr__ следующим образом -  автоматически вызывается при удалении свойства item (не важно: существует оно или нет)
    def __delattr__(self, item):
        print("__delattr__: "+item)

pt = Point4(77, 88)                         # вызвали конструктор, когда создали экземпляр классa pt
                                            # проверили работоспособность программы - Process finished with exit code 0
print( pt.getCorrds() )                     # вызов метода getCorrds - возвращает кортеж этих значений
print( pt._Point4__x )                      # когда метод getCorrds обращается к частной переменной x, то перегруженный метод __getattribute__
                                            # возвращает для него строчку - "Private attribute - частная переменная"


# Зададим в классе Point4 константу WIDTH = 5 со значением 5, и сделаем так, чтоб извне не могли менять
#pt.WIDTH = 6                               # возникает исключение AttributeError
# сработал метод __setattr__, прошла проверка if key == "WIDTH":, было сгенерировано исключение - raise AttributeError
# Таким образом мы запретили менять св-во WIDTH

# метод __getattr__ - попытаемся обратиться к несуществующему св-ву
pt.zzz                                       # __getattr__: zzz - вызовется метод

# метод __delattr__ - создадим св-во, а затем удалим его
pt.z = 1
del pt.z                                     # __delattr__: z - вызовется метод





# дополнительный контроль за локальными свойствами экземпляров классов можно сделать с помощью коллекции
# __slots__ = ["__x", "__y"]
class Point5:
    WIDTH = 5
    # Зададим в классе Point4 константу WIDTH = 5 со значением 5, и сделаем так, чтоб извне не могли менять
    # не можем указывать атрибуты WIDTH=5 которые присутствуют в классе Point5  __slots__ =
    # ValueError: ["__x", "__y", "z", "WIDTH"]

    __slots__ = ["__x", "__y", "z" ]
    # дополнительный контроль за локальными свойствами экземпляров классов можно сделать с помощью коллекции _slots__
    # можем прописать разрешенные св-ва экземпляров класса. В экземплярах локальные св-ва могут быть только такими, которые прописываем
    # можем указывать любые локальные св-ва экземпляров класса
    # создали дополнительное св-во с именем z, то будет ошибка, т.к. pt.z = 9
    # z не находится в разрешенных св-вах  __slots__ = ["__x", "__y"]
    # AttributeError: __slots__ = ["__x", "__y"]


    def __init__(self, x = 0, y = 0):       # объявлен один конструктор
        self.__x = x; self.__y = y          # два private атрибута

    def __checkValue (x):                   # объявлен приватный метод __checkValue внутри класса Point, который будет проверять наше значение x, является ли оно числом или не является
                                            # self - не пишется, т.к. сам метод не будет обращаться к какой-либо локальной переменной экземпляра нашего класса
        if isinstance(x, int) or isinstance(x, float): # с помощью ф-ции isinstance проверяем, что x является типом int или float и тоже самое для y
            return True                     # если это так возвращаем True
        return False                        # иначе возвращено False


# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый setCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def setCorrds(self, x, y):              # set - сеттер - означает установить, задать, координату Corrds, два параметра x, y
        if Point5.__checkValue(x) and Point5.__checkValue(y):  # чтобы обратиться к методу __checkValue у которого нет параметра self,
                                                               # мы должны записать его через имя класса Point3.__checkValue(x)
                                                               # имя класса Point3. приватный метод __checkValue()  (х) передаем ту переменную которую проверяем
            self.__x = x
            self.__y = y
        else:
            print("Координаты должны быть числами")

# Внутри класса Point1 создадим спец. метод -  для записи другого значения, если хотим изменить координату
# Получился метод публичный, открытый getCorrds через который мы можем менять значение закрытых атрибутов __x, __y
    def getCorrds(self):                    # get - геттер
        return self.__x, self.__y           # будем возвращать кортеж этих значений

pt = Point5(7, 8)                           # вызвали конструктор, когда создали экземпляр классa pt
                                            # проверили работоспособность программы - Process finished with exit code 0

                                            # создали дополнительное св-во с именем z, то будет ошибка, т.к.
pt.z = 9                                    # z не находится в разрешенных св-вах  __slots__ = ["__x", "__y"]
                                            # AttributeError: __slots__ = ["__x", "__y"]
                                            # Ошибки не будет: __slots__ = ["__x", "__y", "z"]