Короткий ответ
Дескриптор позволяет один раз описать логику валидации/преобразования и переиспользовать её декларативно на множестве полей многих классов без повторения кода property для каждого поля, но переносит часть логики на менее очевидный уровень протокола __get__/__set__, что увеличивает порог входа для разработчиков, не знакомых с этим механизмом, и усложняет отладку по сравнению с явным property на каждом поле.
Как это работает подробнее
Дескриптор — объект, определяющий __get__, __set__ и/или __delete__, который при присвоении атрибуту класса (не экземпляра) перехватывает доступ к этому имени у всех экземпляров класса: instance.field фактически вызывает Field.__get__(descriptor, instance, owner), а instance.field = value — Field.__set__(descriptor, instance, value). В отличие от property, которая создаётся заново для каждого атрибута через декоратор прямо в классе, дескриптор — это отдельный переиспользуемый класс, который можно параметризовать (тип, диапазон значений, обязательность) и применять к множеству полей разных классов одной и той же реализацией.
Пример кода
class PositiveNumber:
def __set_name__(self, owner, name):
self.private_name = f"_{name}"
def __get__(self, instance, owner):
return getattr(instance, self.private_name, None)
def __set__(self, instance, value):
if value <= 0:
raise ValueError(f"Значение должно быть положительным, получено {value}")
setattr(instance, self.private_name, value) # хранится в __dict__ экземпляра!
class Product:
price = PositiveNumber()
stock = PositiveNumber()
def __init__(self, price, stock):
self.price = price
self.stock = stock
p1 = Product(100, 5)
p2 = Product(200, 10)
print(p1.price, p2.price) # 100 200 — значения независимы по экземплярамЧто использовать на практике
- Практика. Дескрипторы оправданы, когда одинаковая логика валидации/преобразования повторяется на множестве полей (например,
PositiveNumber,NonEmptyString,ForeignKeyFieldв ORM) — это именно тот сценарий, которым пользуются настоящие ORM и сериализаторы (Django ORM, SQLAlchemy, Pydantic во многом построены на похожих идеях). Если валидация специфична для одного-двух полей конкретного класса, property остаётся проще для чтения и сопровождения, так как логика видна прямо в месте использования, а не в отдельном переиспользуемом классе дескриптора.
Подводные камни
- Риски. Забывают разницу между дескриптором данных (определяет
__set__, имеет приоритет над__dict__экземпляра) и недескриптором/дескриптором не-данных (только__get__), из-за чего получают неожиданное поведение при попытке переопределить значение атрибута на уровне экземпляра. Также хранят состояние поля непосредственно в самом объекте-дескрипторе (который один на класс, а не на экземпляр) вместоinstance.__dict__— это приводит к тому, что значение поля становится общим для всех экземпляров класса, повторяя классическую ошибку общего mutable-состояния, но на уровне протокола дескрипторов, что сложнее диагностировать. - Компромисс. Выигрыш в переиспользуемости и декларативности достаётся по цене дополнительного уровня абстракции, который часть команды может плохо понимать, что увеличивает время онбординга и риск тонких багов (особенно с хранением состояния по экземплярам, а не по классу). Решение должно учитывать, сколько полей и классов реально выигрывают от переиспользования логики, и готова ли команда инвестировать в обучение и документирование этого внутреннего слоя — для небольшого числа полей цена абстракции может не оправдаться.