Дескрипторы

Класс у которого переопреден один из методов:
__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:

  1. Получим значение атрибута, some_variable = obj.a
  2. Изменим его значение, obj.a = 'new value'
  3. Удалим атрибут, 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/