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