"""Константы в Python?
В отличие от некоторых других языков, в Питоне нет возможности объявить
неизменяему переменную. Для обозначения переменных, значения которых не должны 
меняться существует договорённость именовать их прописными буквами.
"""
# MY_CONSTANT = 'моя «константа»'
# print(MY_CONSTANT)
# # > моя «константа»
# MY_CONSTANT = 'беспрепятственно изменяем значение константы'
# print(MY_CONSTANT)
# > беспрепятственно изменяем значение константы


"""Область видимости. Что такое локальные и глобальные переменные?
В Python, переменные, на которые только ссылаются внутри функции/метода, 
считаются глобальными. Если переменной присваивается новое значение где-либо в 
теле функции/метода, считается, что она локальная, и, если вам нужно, то нужно явно 
указывать её глобальной.
"""
# y = 1
# def my_func():
#     print(y)
#     x = y
#     print(x)
#     # y = 5
#     print(y)
#     global z
#     z = 10
#
# my_func()
# print(z)


"""Что такое self и для чего он нужен?
Параметр self в данном случае это ссылка на конкретный экземпляр класса и 
нужен для того, чтобы объект мог обратиться к собственным методам или 
свойствам: self.имя_свойства. 
__dict__ в данном случае выведит все локальные свойства и методы относящиеся к
объекту в отношении которого он вызывается.
"""
# class Point:
#     def setCoords(self):
#         # Выведем список локальных переменных self
#         print(self.__dict__)
#         print(self.x, self.y)
#
#
# # Создаём (инстанцируем) экземпляр (объект) класса Point
# object_point = Point()
# object_point1 = Point()
# object_point3 = Point()
# # Создаём локальные переменные.
# object_point.x = 5
# object_point.y = 10
#
# Point.setCoords(object_point)
#
# # Обращаемся к методу через созданный объект object_point, в данном случае в
# # self ссылка на объект object_point попадёт автоматически.
# object_point.setCoords()
# # Обращаемся к методу через имя класса,
# # в данном случае в self ссылка на объект object_point попадёт в явном виде.
# Point.setCoords(object_point)


"""Для чего нужен super()? 
Функция super используется если мы переопределяем 
у ребёнка метод, который был у родителя, но хотим чтобы логика родителя 
осталась, мы вызываем super, тем самым говоря интерпретатору питона: возьми 
у родителя следующий метод (или атрибут). В примере, функцией super() мы  
вызываем родительский конструктор внутри дочернего класса тем самым получая 
доступ к его свойствам. Делается это для того чтобы не укаказывать 
(переписывать свойства). Через super() таким образом можно обратится к любому 
методу или свойству родителя, за исключением приватных (__)"""
# class Computer:
#     def __init__(self, computer, ram, ssd):
#         self.computer = computer
#         self.ram = ram
#         self.ssd = ssd
#
#     def __del__(self):
#         print(1)
#
# x = Computer('PC', '256', '100')


#
#
# class Laptop(Computer):
#     def __init__(self, computer, ram, ssd, model):
#         super().__init__(computer, ram, ssd)
#         self.model = model
#
#
# lenovo = Laptop('lenovo', 2, 512, 'l420')
# print('This computer is:', lenovo.computer)
# print('This computer has ram of', lenovo.ram)
# print('This computer has ssd of', lenovo.ssd)
# print('This computer has this model:', lenovo.model)


"""Зчем нужна конструкция if __name__ == '__main__':?
Её основное назначение — разделение кода, который будет выполнятся при 
вызове кода как модуля (при импортировании его в другой скрипт) — и при 
запуске самого модуля, как отдельного файла. Для того чтобы убедится что 
func1() выполнится а func() не выполнится при импорте запустите # Script 2 из 
файла import_script.py
"""
# print('Script 1. My name is: %s' % __name__)
# print('This is simple code from script 1')
#
#
# def func():
#     print('This is code from function from script 1 using construction '
#           'if __name__ == "__main__"')
#
#
# def func1():
#     print('This is code from function from script 1 NOT using construction '
#           'if __name__ == "__main__"')
#
#
# func1()
#
# if __name__ == "__main__":
#     func()


"""Разница между атрибутами класса, атрибутами экземпляра?
Атрибуты класса одинаковы для всех экземпляров класса, тогда как атрибуты
экземпляров являются особыми для каждого экземпляра. Атрибуты экземпляра
предназначены для данных, специфичных для каждого экземпляра и атрибутов
класса, которые должны использоваться всеми экземплярами класса.
"Методы экземпляра" - это специфические атрибуты класса, которые принимают
экземпляр класса как первый атрибут и предполагают манипулировать этим 
экземпляром. "Методы класса" - это методы, определенные в классе, которые 
принимают класс как первый атрибут не экземпляр (поэтому методы класса).
Вы можете легко увидеть атрибуты класса, обратившись к A.__dict__:.
"""
# class A:
#     class_attribute = 10
#
#     def class_method(self):
#         self.instance_attribute = 'I am instance attribute'
#
#
# print(A.__dict__)
# # {'__module__': '__main__', 'class_attribute': 10,
# # 'class_method': <function A.class_method at 0x00000267C0A7A160>,
# # '__dict__': <attribute '__dict__' of 'A' objects>,
# # '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
#
# a = A()
# a.class_method()
# a.class_attribute = 11
# print(a.__dict__)
# # {'instance_attribute': 'I am instance attribute', 'class_attribute': 11}

"""Как можно работать с методами без возможности их сокрытия. И насколько 
актуальны в этом случае геттеры и сеттеры?
Инкапсуляция в Python работает лишь на уровне соглашения между программистами
о том, какие атрибуты являются общедоступными, а какие — внутренними.

Одиночное подчеркивание (protected) в начале имени атрибута говорит о том, что
переменная или метод не предназначен для использования вне методов класса,
однако атрибут доступен по этому имени, ограничение на уровне соглашения.

Двойное подчеркивание (private) в начале имени атрибута даёт большую защиту:
атрибут становится недоступным по этому имени из вне класса физически.

В данном примере:
Метод _get_age() является protected, но мы можем к нему обращается через
объект или self. Программист, называя метод или свойство начиная с "_",
хочет сказать "лучше сюда не лезть" но на практике нам не чего не мешает 
менять данные свойства.

Метод __get_weight() является private, и мы не можем к нему обращается через
объект или self вне класса где он был создан. Программист, называя метод или
свойство начиная с "__" полностью запрещает к нему доступ на уровне
интерпретатора.
jack.__get_weight() # Попытка обращения к методу вызовет ошибку.
Существует способ обойти private ограничение и вызвать __get_weight() так:
jack._Horse__get_weight()  # Но лучше этого не делать.
"""
# # Пример 1
# class Animal:
#     def __init__(self, name, age, weight):
#         # объявляем публичные свойства
#         self.name = name
#         self.age = age
#         self.weight = weight
#         # объявляем свойства с ограничением доступа
#         self._protected_value = True  # объявляем protected свойство
#         self.__private_value = True  # объявляем private свойство
#
#     def voice(self):
#         print('ИгоГо')
#
#
# class Horse(Animal):
#     def voice(self):
#         self.voice()
#
#     def __get_age(self):
#         print(self.age)
#
#     def _get_weight(self):
#         print(self.weight)
#
#
# jack = Horse('Jack', 6, 2)
# jack._get_weight() # Попытка обращения к методу protected не вызовет ошибки
# print(jack._protected_value) # тоже самое с protected свойствами
# jack.__get_age() # Попытка обращения к методу private вызовет ошибку
# print(jack.__private_value) # тоже самое с private свойствами
# jack._Horse__get_age()  # Обход ограничения private метода
# print(jack._Animal__private_value)  # Обход ограничения private свойства

# # Пример 2 Гетеры и Сетеры
# class Person:
#     def __init__(self, name):
#         self.__name = name      # устанавливаем имя
#         self.__age = 1          # устанавливаем возраст
#
#     def set_age(self, age):
#         if age in range(1, 100):
#             self.__age = age
#         else:
#             print("Недопустимый возраст")
#
#     def get_age(self):
#         return self.__age
#
#     def get_name(self):
#         return self.__name
#
#     def display_info(self):
#         print("Имя:", self.__name, "\tВозраст:", self.__age)
#
# tom = Person("Tom")
#
# tom.display_info()          # Имя: Tom  Возраст: 1
# tom.set_age(-3486)          # Недопустимый возраст
# tom.set_age(25)
# tom.display_info()          # Имя: Tom  Возраст: 25
""" Инкапсуляция, гетеры и сетеры в Python подробнее по ссылкам: 
https://metanit.com/python/tutorial/7.2.php
https://proproprogs.ru/python_oop/rezhimy-dostupa-gettery-i-settery
"""

"""Как принято в Питоне поступать при вызове конструктора с некорректными 
данными? Вызывать исключение или что-то ещё?
> Решается вызовом исключений. См пример:
"""
# class Example:
#
#     def __init__(self, num, price, code, year):
#         # 1
#         self.num = int(num)
#
#         # 2
#         self.set_price(price)
#
#         # 3
#         try:
#             self.code = int(code)
#         except ValueError:
#             raise ValueError('The code "%s" should be int().'
#                              % code)
#         # 4
#         if type(year) == int:
#             self.year = year
#         else:
#             raise ValueError('The year "%s" should be int().'
#                              % year)
#
#     def set_price(self, price):
#         try:
#             self._price = float(price)
#         except ValueError:
#             raise ValueError('The price "%s" should be float().'
#                              % price)


"""Насколько применимо в python множественное наследование и актуально ли оно?
> Актуально, но злаупотреблять им не следует, т.к. сильно усложняет код.
"""

"""Как правильно интерпретировать понятие "интерфейс" в Python и в частности 
в ООП? Можно ли привести конкретные примеры?
В python нет специального вида класа как интерфейс, вы можете реализовать его 
только самостоятельно обычно это делается на базе входящей в комплект 
библиотеки abc (Abstract Base Class) (далее как Абстрактные базовые классы.)
Но есть интерфейс объекта - это набор методов и атрибутов конкретного объекта, 
которые описываются в класее на базе которого создаётся этот объект.
Абстрактные базовые классы и интерфейсы — близкие по назначению и смыслу 
сущности. Как первые, так и вторые представляют собой своеобразный способ 
документирования кода и помогают ограничить взаимодействие 
отдельных абстракций в программе (классов).
В Python мы можем использовать абстрактный базовый класс для определения и 
применения интерфейса.
Абстрактным называется класс, который содержит один и более абстрактных 
методов. Абстрактным называется объявленный, но не реализованный метод. 
Абстрактные классы не могут быть инстанциированы, от них нужно унаследовать, 
реализовать все их абстрактные методы и только тогда можно создать экземпляр 
такого класса. В Python отсутствует встроенная поддержка абстрактных классов, 
для этой цели используется модуль abc (Abstract Base Class)
http://pythonicway.com/education/python-oop-themes/33-python-abstract-class
"""
# # Пример реализации интерфейса на базе Абстрактного класса
# from abc import ABC, abstractmethod
#
#
# class ChessPiece(ABC):
#     # общий метод, который будут использовать все наследники этого класса
#     def draw(self):
#         print("Drew a chess piece")
#
#     # абстрактный метод, который будет необходимо переопределять для каждого
#     # подкласса
#     @abstractmethod
#     def move(self):
#         pass
#
#
# class Queen(ChessPiece):
#     def move(self):
#         print("Moved Queen to e2e4")
#
# # Мы можем создать экземпляр класса
# q = Queen()
# # И нам доступны все методы класса
# q.draw()
# q.move()

"""Чем вызвана (непонятная для меня) необходимость постоянно в методах в 
качестве первого аргумента указывать ссылку на текущий экземпляр класса/класс? 
(При этом при вызове метода аргумент self/cls куда-то пропадает и таким 
образом в метод передаются только последующие аргументы.)
> Параметр self это ссылка на конкретный экземпляр класса и 
нужен для того, чтобы можно было обращаться к свойствам и методам конкретного
объекта self.имя_свойства внутри класса. self подразумевает, что метод связан с 
экземпляром класса. В то время как cls (Имя для аргумента, представляющего 
текущий класс) подразумевает,что метод принадлежит классу.
--------------
dog: у котов внутри есть мурчалка
dog: она реализована для всех котов в классе кот
dog: в объекте кот надо как то вызвать метод мурчало у класса кот
dog: как ты это сделаешь?
dog: кот.мурчало()
dog: ежели ты вызовешь кот.мурчало(), муркнут сразу все коты на свете
dog: а ежели ты вызовешь self.мурчало(), муркнет только тот кот, на которого указывает self
"""



# Полезные ссылки
"""
Что такое классы в Python:
https://python-scripts.com/python-class

Как работают классы в Python:
https://medium.com/@melevir/%D0%BA%D0%B0%D0%BA-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%8E%D1%82-%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D1%8B-%D0%B2-python-f8ba90fe2f8e

Вот тут с примерами можно почитать что такое self и для чего он нужен 
https://pyneng.readthedocs.io/ru/latest/book/25_oop_basics/parameter_self.html
"""



