Connect with us

Обучение

Заметки Python #25: Дескрипторы, метаклассы

536

Данная тема считается одной из самых сложных и непонятных для 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__ — создает и возвращает новый экземпляр класса

Нажмите что бы оставить комментарий

Ответить

Ваш адрес email не будет опубликован.

Лучшие сервисы стриминга музыки в 2019 году

Сервисы

Телевидение Wink Ростелеком: Samsung LG, Sony, Phillips, Android TV

Ростелеком

Ноутбуки Asus не видят жесткий диск. Автоматический вход в BIOS при старте

Гаджеты

LG WEB OS: приложения, обновления, настройка, проблемы со звуком

Гаджеты

Advertisement Яндекс.Метрика

Digital2.ru - Тренды, Профессии IT, WEB- разработка, Вакансии, Автоматизация, Цифровая экономика
Свободное копирование и распространение материалов с сайта Digital2.ru
разрешено только с указанием активной ссылки на Digital2 как на источник.
Copyright 2018 - 2020 © All rights reserved

OPGIO.COM

Connect