Разберите механизм доступа к атрибуту: какое значение напечатает код и почему запись в __dict__ не меняет результат?
class D:
def __get__(self, obj, owner):
return 'descriptor'
def __set__(self, obj, value):
raise AttributeError('read only')
class C:
x = D()
c = C()
c.__dict__['x'] = 'instance'
print(c.x)
Код напечатает descriptor. Объект D является дескриптором данных, потому что реализует __set__, поэтому при доступе к c.x он имеет приоритет над c.__dict__.
Дескрипторы — часть объектной модели Python, предназначенная для унификации доступа к атрибутам. Они позволяют классу управлять чтением, записью и удалением атрибутов без специальной логики в каждом пользовательском классе.
На этом механизме построены, в частности, property, методы экземпляра, многие инструменты ORM и поля с валидацией. Дескриптор отделяет логику доступа от места, где атрибут используется.
В c.__dict__ вручную создан атрибут x со значением instance, но в самом классе C также есть атрибут x, являющийся дескриптором. Нужно определить, какой источник будет выбран при выражении c.x.
Неверное предположение состоит в том, что атрибут экземпляра всегда перекрывает атрибут класса. Для дескрипторов это зависит от того, реализуют ли они __set__ или __delete__.
Вызов c.x обрабатывается специальным методом C.__getattribute__. Упрощённый порядок поиска таков:
x.__get__ и __set__ либо __delete__, он считается дескриптором данных и вызывается первым.c.__dict__.__set__ и __delete__ или найден атрибут класса.В примере D реализует __get__ и __set__, поэтому D — дескриптор данных. Выражение c.x фактически приводит к вызову D.__get__(c, C), а значение из c.__dict__['x'] игнорируется.
При обращении через класс Item.value дескриптор обычно получает obj=None. Это позволяет вернуть сам дескриптор или специальное представление для интроспекции.
Если убрать __set__ из D, дескриптор станет дескриптором без данных. Тогда c.__dict__['x'] будет найден раньше, и c.x вернёт instance. Это ключевое практическое различие.
Описанный порядок относится к обычному доступу через __getattribute__. Пользовательская реализация __getattribute__ может изменить поведение, но при этом обязана осторожно вызывать super().__getattribute__, иначе легко получить рекурсию или сломать стандартный поиск атрибутов.
В ORM поле модели должно проверять тип значения и одновременно скрывать способ его хранения. Например, публичное поле user.age хранится во внутреннем словаре, а дескриптор проверяет, что возраст является целым числом.
Рассматривались два варианта. Переопределение __getattribute__ даёт полный контроль, но усложняет весь механизм доступа и повышает риск рекурсии. Свойство property проще для одного конкретного класса, однако при большом числе однотипных полей приводит к повторению кода.
Выбран дескриптор данных: он централизует чтение и запись, гарантирует, что прямое значение в __dict__ не обойдёт валидацию, и может переиспользоваться в разных моделях. Цена решения — более сложная модель поиска атрибутов и необходимость учитывать поведение при доступе через класс.
Что изменится, если у дескриптора останется только __get__?
Он станет дескриптором без данных. При c.x сначала будет проверен c.__dict__, поэтому значение instance перекроет результат дескриптора. Такой вариант полезен, например, для ленивого вычисления, которое можно заменить значением экземпляра.
Почему наличие метода __set__, который всегда выбрасывает исключение, всё равно влияет на приоритет?
Для определения дескриптора данных важно само наличие __set__ или __delete__, а не успешность операции записи. Поэтому даже read-only-дескриптор остаётся дескриптором данных и продолжает иметь приоритет над __dict__.
Что произойдёт при обращении к C.x, а не к c.x?
Python всё равно вызывает D.__get__, но передаёт obj=None и owner=C. Дескриптор должен явно обработать этот случай. Обычно он возвращает себя, чтобы класс можно было инспектировать, либо предоставляет метаданные поля. Если этот случай не учесть, обращение через класс может завершиться ошибкой или вернуть неподходящее значение.