Python

Python descriptor protocol: понятное объяснение

Descriptor protocol в Python — это механизм, который позволяет объекту управлять доступом к атрибуту другого объекта. Он работает через методы __get__, __set__ и __delete__

На descriptor protocol построены property, методы классов, staticmethod, classmethod и многие ORM-поля. Тему лучше изучать после обычных классов, атрибутов и @property

По этой теме полезно отдельно посмотреть Составьте еженедельное расписание занятий, чтобы расширить контекст и сравнить подходы

По этой теме полезно отдельно посмотреть Аннотации типов для декораторов в Python, чтобы расширить контекст и сравнить подходы

Самый короткий descriptor

class Descriptor:
    def __get__(self, obj, owner):
        return 'значение из descriptor'

class User:
    name = Descriptor()

user = User()
print(user.name)

Вывод:

значение из descriptor

Когда Python видит user.name, он находит name в классе User, замечает у объекта метод __get__ и вызывает его

Что значат obj и owner

class Descriptor:
    def __get__(self, obj, owner):
        print(obj)
        print(owner)
        return 'ok'

class User:
    name = Descriptor()

user = User()
print(user.name)

obj — конкретный экземпляр, например user

owner — класс, например User

Если обратиться к атрибуту через класс:

print(User.name)

obj будет None, а owner останется классом User

Descriptor с проверкой значения

class PositiveNumber:
    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    def __get__(self, obj, owner):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError('число должно быть положительным')

        setattr(obj, self.private_name, value)

class Product:
    price = PositiveNumber()

product = Product()
product.price = 100

print(product.price)

__set_name__ вызывается при создании класса и помогает descriptor узнать имя атрибута, к которому его привязали

В примере price хранится внутри объекта как _price, а доступ product.price проходит через descriptor

Зачем это нужно

Descriptor полезен, когда одна и та же логика атрибута нужна во многих классах или полях

Например:

  • проверка положительного числа
  • проверка непустой строки
  • ленивое вычисление значения
  • преобразование типа
  • ORM-поле модели
  • кешируемое свойство

Если логика нужна только для одного атрибута в одном классе, чаще достаточно @property

Непустая строка через descriptor

class NonEmptyString:
    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    def __get__(self, obj, owner):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError('нужна строка')

        if not value.strip():
            raise ValueError('строка не должна быть пустой')

        setattr(obj, self.private_name, value.strip())

class User:
    name = NonEmptyString()

user = User()
user.name = ' Анна '

print(user.name)

Такую проверку можно переиспользовать в разных классах

Data descriptor и non-data descriptor

Если descriptor определяет __get__ и __set__, это data descriptor

Если descriptor определяет только __get__, это non-data descriptor

Разница важна при поиске атрибутов. Data descriptor имеет приоритет над значением в __dict__ объекта. Non-data descriptor может быть перекрыт атрибутом экземпляра

На старте достаточно запомнить проще: если descriptor должен контролировать запись значения, нужен __set__

Сравнение с property

@property удобен для одного класса

class Product:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value <= 0:
            raise ValueError('цена должна быть положительной')

        self._price = value

Descriptor удобен, когда эту проверку нужно применить к нескольким атрибутам или классам

Частые ошибки

  • изучать descriptor до понимания обычных классов
  • использовать descriptor там, где проще @property
  • забывать, что descriptor должен лежать на уровне класса
  • хранить значение прямо внутри descriptor и случайно делить состояние между объектами
  • не учитывать обращение через класс, где obj равен None
  • путать data descriptor и non-data descriptor

Что читать дальше

Перед descriptor полезно уверенно понимать Классы в Python: простой пример

Для соседней темы специальных методов посмотрите Python __getitem__: что это и как работает

Descriptor protocol позволяет объекту управлять чтением, записью и удалением атрибута через __get__, __set__ и __delete__

Это мощный механизм для библиотек, ORM и повторяемой логики атрибутов. В обычном прикладном коде сначала проверьте, не достаточно ли @property

Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные

Настройки cookie

0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x