Класс у которого переопреден один из методов:
__get__ или __set__ или __delete__ — является дескриптором
Если переопределен только метод __get__ то — это nondata дескриптор
Если переопределен только метод __set__ или __delete__ то — это data дескриптор
class Descriptor: def __get__(self, obj, obj_type): print('get') def __set__(self, obj, value): print('set') def __delete__(self, obj): print('delete') class Product: attr = Descriptor() instance = Product() instance.attr # >>> get instance.attr = 10 # >>> set
Определение дескриптора:
Дескриптор это атрибут объекта со “связанным поведением”, то есть такой атрибут, при доступе к которому его поведение переопределяется методом протокола дескриптора. Эти методы
__get__
,__set__
и__delete__
. Если хотя бы один из этих методов определен в объекте , то можно сказать что этот метод дескриптор.
Вы видели похожий код или, может быть даже писали что-то подобное?
from sqlalchemy import Column, Integer, String class User(Base): id = Column(Integer, primary_key=True) name = Column(String)
Этот небольшой фрагмент был частично взят из учебника по популярной ORM библиотеки SQLAlchemy. Подобный код можно встреть наверно в любой ORM в python.
А вы когда-нибудь задумывались, почему атрибуты id
и name
не передаются через метод __init__
и потом не привязываются к экземпляру класса, как это обычно делается в классе. Если да то в этой статье я расскажу, как и зачем это делается.
В python существует три варианта доступа к атрибуту. Допустим у нас есть атрибут a
объекта obj
:
- Получим значение атрибута,
some_variable = obj.a
- Изменим его значение,
obj.a = 'new value'
- Удалим атрибут,
del obj.a
Python позволяет перехватить выше упомянутые попытки доступа к атрибуту и переопределить связанное с этим доступом поведение. Это реализуется через механизм протокола дескрипторов.
Пример без использования дескрипторов
Итак у нас есть код:
class Order: def __init__(self, name, price, quantity): self.name = name self.price = price self.quantity = quantity def total(self): return self.price * self.quantity apple_order = Order('apple', 1, 10) apple_order.total() # 10
В этом коде данные никак не проверяются, т.е. например «количеству» (quantity) можно выставить отрицательное значение. Очевидным вариантом решения проблемы будет, сделать из атрибута quantity — свойтсво @property. Реализуем это:
class Order: def __init__(self, name, price, quantity): self._name = name self.price = price self._quantity = quantity # (1) @property def quantity(self): return self._quantity @quantity.setter def quantity(self, value): if value < 0: raise ValueError('Cannot be negative.') self._quantity = value # (2) ... apple_order.quantity = -10 # ValueError: Cannot be negative
Теперь при установке нового значения свойству quantity — происходит проверка на не отрицательное значение. Всё хорошо, но есть одна проблема: тоже самое необходимо проделать с атрибутом price, а что если подобных атрибутов будет 20 ?!
Пример использования дескрипторов
Перепишем пример выше, с использованием дескрипторов (Python > 3.6)
class NonNegative: def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): if value < 0: raise ValueError('Cannot be negative.') instance.__dict__[self.name] = value def __set_name__(self, owner, name): self.name = name class Order: price = NonNegative() quantity = NonNegative() def __init__(self, name, price, quantity): self._name = name self.price = price self.quantity = quantity def total(self): return self.price * self.quantity apple_order = Order('apple', 1, 10) apple_order.total() # 10 apple_order.price = -10 # ValueError: Cannot be negative apple_order.quantity = -10 # ValueError: Cannot be negative
В данном примере мы создали новый класс NonNegative
— который реализует протокол дескриптора т.е. методы __get__ и __set__ Метод object.__set_name__(self, owner, name)
— вызывается во время создания класса. В этом случае дескриптор назначается на имя атрибута.
По материалам: https://webdevblog.ru/chto-takoe-deskriptory-i-ih-ispolzovanie-v-python-3-6/