Данная тема считается одной из самых сложных и непонятных для junior-разработчиков.
Поехали!
Дескриптор
Это атрибут объекта с некоторым заскриптованным поведением. При доступе к этому атрибуту его поведение меняется на то, что задано методом дескриптора. Это могут быть встроенные методы __get__ , __set__, __delete__. Для наглядности представим, что у нас есть некий объект example — это экземпляр класса. Чтобы получить значение его атрибута мы пишем обращаемся через этот экземпляр класса: x = example.attribute. Эта конструкция отвечает за метод __get__ (т.е. получаем значение атрибута). Для того, чтобы изменить атрибут, нужно присвоить новое значение example.attribut = ‘newattribut’.
Дескрипторы — это, своего рода, декораторы для атрибутов
Теперь в работу включился метод __set__. Удаление соответственно делается через __delete__. Суть в том, что мы можем перехватить доступ к атрибуту и переопределить его поведение. Это позволит инкапсулировать атрибуты и проверять их значения. Всё это очень сильно смахивает на декораторы, где мы меняли поведение функции. Как это выглядит на практике? Для начала простой пример
[code]
class Whisky:
def __init__(self, name, type, cost, size): # параметры, являющиеся атрибутами класса Вискаря
self.name = name
self.type = type
self.cost = cost
self.size = size
def total_cost(self):
return self.cost * self.size
alco = Whisky(‘Lawsons’, ‘Spiced’, 1200, 0.7) # создаем экземпляр класса, вызываем метод и перадаем атрибуты
print(alco.total_cost())
# теперь пробуем изменить атрибуты (если мы не используем слоты)
alco.cost = -10
alco.size = 100
print(alco.total_cost())
[/code]
Результат:
Хотя скрипт и отработал корректно, но проблема данного примера в том, что атрибуты не проверяются, поэтому стоимость будет неправильной. Чтобы это исправить будем использовать дескрипторы и преобразим наш пример.
[code]
class Whisky:
def __init__(self, name, type, cost, size): # параметры, являющиеся атрибутами класса Вискаря
self.name = name
self.type = type
self._cost = cost # формальная инкапсуляция
self._size = size
@property
def cost(self): # декоратор проперти для использования метода как атрибута
return self._cost
@cost.setter # как мы помним, set — это изменение поведения атрибута
def cost(self, value): # и тут мы уже проверяем корректности значения атрибута
if value < 0:
raise ValueError(«Стоимость за литр должна быть положительной, алло»)
self._cost = value
@property
def size(self): # опять преобразовали в свойство с помощью проперти
return self._size
@size.setter
def size(self, value):
if value <= 0:
raise ValueError(«Где ты видел 0 литров спайседа, чувак?»)
self._size = value
def total_cost(self):
return self.cost * self.size
alco = Whisky(‘Lawsons’, ‘Spiced’, 1200, 0.7) # создаем экземпляр класса, вызываем метод и перадаем атрибуты
print(alco.total_cost())
# теперь пробуем изменить атрибуты (если мы не используем слоты) и получим ошибку!
alco.cost = 900
alco.size = 0
print(alco.total_cost())
[/code]
Теперь отрицательное значение будет выдавать ошибку. Приложение отказывается работать, если мы стараемся некорректно переопределить значение атрибута. Но и этот код можно упростить, т.к. атрибутов может быть много и если для каждого писать преобразование в свойство, то код станет некомпактным. Как это исправить?
[code]
class ChangeAttr: # создаем свой класс и пишем протокол дескриптора
def __init__(self, here_attr): # переданные значения из экземпляров класса будут в конструкторе
self.here_attr = here_attr # here_attr — это любой переданный атрибут
def __get__(self, example, owner): # owner — это «владелец» (класс), example — это экземпляр
return example.__dict__[self.here_attr] # добавляем атрибут в список атрибутов класса
def __set__(self, example, value): # проверяем на исключение
if value <= 0:
raise ValueError(«Должен быть положительным»)
example.__dict__[self.here_attr] = value # записываем значение в словарь
class Whisky:
cost = ChangeAttr(‘cost’) #создаем экземпляр класса ChangeAttr (на вход передаем параметры)
size = ChangeAttr(‘size’)
def __init__(self, name, type, cost, size): # параметры, являющиеся атрибутами класса Вискаря
self.name = name
self.type = type
self.cost = cost
self.size = size
def total_cost(self):
return self.cost * self.size
alco = Whisky(‘Lawsons’, ‘Spiced’, 1200, 0.7) # создаем экземпляр класса, вызываем метод и перадаем атрибуты
print(alco.total_cost())
# теперь пробуем изменить атрибуты (если мы не используем слоты) и получим ошибку!
alco.cost = 900
alco.size = 0
print(alco.total_cost())
[/code]
Здесь у нас атрибуты класса Whisky попадают в протокол дескриптора, где мы явно определяем их извлечение и присваивание. __get__ — извлечение значения по ключу из словаря атрибутов класса, __set__ — присваивание значения по ключу нужному атрибуту класса.
Данный способ был актуален до версии питона 3.5. Теперь можно использовать другой подход, а если точнее — специальный протокол __set_name__, который позволяет отказаться от передачи значений. Его синтаксис: object.__set_name__(self, owner, name). Дескриптор в этом случае назначается на name (имя атрибута). Как это поможет оптимизировать наш код?
[code]
class ChangeAttr:
def __get__(self, instance, owner):
return instance.__dict__[self.my_attr] def __set__(self, instance, value):
if value < 0:
raise ValueError(«Должно быть положительным»)
instance.__dict__[self.my_attr] = value
def __set_name__(self, owner, my_attr): # привязываем имя атрибута к дескриптору
self.my_attr = my_attr
class Whisky:
cost = ChangeAttr() #создаем экземпляр класса ChangeAttr (на вход ничего не передаем)
size = ChangeAttr()
def __init__(self, name, type, cost, size): # параметры, являющиеся атрибутами класса Вискаря
self.name = name
self.type = type
self.cost = cost
self.size = size
def total_cost(self):
return self.cost * self.size
alco = Whisky(‘Lawsons’, ‘Spiced’, 1200, 0.7) # создаем экземпляр класса, вызываем метод и перадаем атрибуты
print(alco.total_cost())
[/code]
На первый взгляд, изменения кажутся незначительными. Однако отсутствие передачи параметров в класс ChangeAttr дает нам возможность вынести дескриптор в отдельный модуль и импортировать его, освобождая основной код
Метаклассы
Это классы, экземпляры которого являются классами. Необходимо запомнить три утверждения:
1. Классы — это тоже объекты.
2. Всё в Python — это объекты.
3. Метаклассы — это создатель класса.
Полная «пищевая» цепочка выглядит как: Метакласс -> Класс -> Объект. Например, у нас есть переменная со значением целого числа — x = 10. Вывод команды print(type(x)) даст нам закономерный итог: <class ‘int’>, целое число. Но что если далее написать print(type(int))? Что выдаст программа? А выдаст она <class ‘type’>. Значение type — это и есть метакласс. С помощью функции type также можно создавать собственные классы: SuperClass = type («SuperClass», (object), {whisky = lawsons, size = 0,5L}), где «SuperClass» — это название класса, (object) — это базовый класс , {whisky = lawsons, size = 0,5L} — атрибуты нового класса
Если мы хотим изменить стандартное поведение класса int, то мы можем создать собственный класс ChangeInt, который будет являться метаклассом, соответственно наследоваться он будет от класса type. Пример:
[code]
class ChangeInt(type): # это наш метакласс. Все экземпляры класса int будут иметь это поведение
def __call__(cls, *args, **kwargs): # метод вызывается, когда необходимо создать объект для уже сущ. класса
print(«Я здесь меняю класс int») #метакласс отвечает за создание объектов
return type.__call__(cls, *args, **kwargs)
class int(metaclass=ChangeInt): # изменяем поведение класса int
def __init__(self, a, b):
self.a = a
self.b = b
i = int(22,24)
print(i.b)
[/code]
Мы видим, что объект теперь создается нашим метаклассом, который меняет структуру вывода (в нашем случае мы просто добавляем строку). Метакласс берет под контроль управление классами.
В чем разница между метаклассом и наследованием?
Наследование — это работа с атрибутами и методами родителя. Метакласс — это изменение поведение класса. Т.е. наследуя, мы просто используем, а метакласс — изменяем.
Методы метакласса
__prepare__ — возвращает словарь для атрибутов класса (изменяем поведение при добавлении атрибутов в словарь)
__new__ — создание и получение нового класса
__init__ — инициализирует новый класс (можно изменить стандартное поведение)
__call__ — создает и возвращает новый экземпляр класса