Программирование PythonМодель данныхРазработчик Python среднего уровня

Разберите механизм доступа к атрибуту: какое значение напечатает код и почему запись в dict не меняет резул...

Разберите механизм доступа к атрибуту: какое значение напечатает код и почему запись в __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)
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Код напечатает descriptor. Объект D является дескриптором данных, потому что реализует __set__, поэтому при доступе к c.x он имеет приоритет над c.__dict__.

Исторический контекст

Дескрипторы — часть объектной модели Python, предназначенная для унификации доступа к атрибутам. Они позволяют классу управлять чтением, записью и удалением атрибутов без специальной логики в каждом пользовательском классе.

На этом механизме построены, в частности, property, методы экземпляра, многие инструменты ORM и поля с валидацией. Дескриптор отделяет логику доступа от места, где атрибут используется.

Постановка проблемы

В c.__dict__ вручную создан атрибут x со значением instance, но в самом классе C также есть атрибут x, являющийся дескриптором. Нужно определить, какой источник будет выбран при выражении c.x.

Неверное предположение состоит в том, что атрибут экземпляра всегда перекрывает атрибут класса. Для дескрипторов это зависит от того, реализуют ли они __set__ или __delete__.

Подробное решение

Вызов c.x обрабатывается специальным методом C.__getattribute__. Упрощённый порядок поиска таков:

  1. В классе ищется атрибут x.
  2. Если найден объект с __get__ и __set__ либо __delete__, он считается дескриптором данных и вызывается первым.
  3. Если дескриптора данных нет, проверяется c.__dict__.
  4. Затем могут быть вызваны обычный дескриптор без __set__ и __delete__ или найден атрибут класса.

В примере D реализует __get__ и __set__, поэтому D — дескриптор данных. Выражение c.x фактически приводит к вызову D.__get__(c, C), а значение из c.__dict__['x'] игнорируется.

class Field: def __get__(self, obj, owner): if obj is None: return self return obj.__dict__.get('_value') def __set__(self, obj, value): if not isinstance(value, int): raise TypeError('нужно целое число') obj.__dict__['_value'] = value class Item: value = Field() item = Item() item.value = 10 print(item.value) # 10

При обращении через класс Item.value дескриптор обычно получает obj=None. Это позволяет вернуть сам дескриптор или специальное представление для интроспекции.

Если убрать __set__ из D, дескриптор станет дескриптором без данных. Тогда c.__dict__['x'] будет найден раньше, и c.x вернёт instance. Это ключевое практическое различие.

Описанный порядок относится к обычному доступу через __getattribute__. Пользовательская реализация __getattribute__ может изменить поведение, но при этом обязана осторожно вызывать super().__getattribute__, иначе легко получить рекурсию или сломать стандартный поиск атрибутов.

Ситуация из практики

В ORM поле модели должно проверять тип значения и одновременно скрывать способ его хранения. Например, публичное поле user.age хранится во внутреннем словаре, а дескриптор проверяет, что возраст является целым числом.

Рассматривались два варианта. Переопределение __getattribute__ даёт полный контроль, но усложняет весь механизм доступа и повышает риск рекурсии. Свойство property проще для одного конкретного класса, однако при большом числе однотипных полей приводит к повторению кода.

Выбран дескриптор данных: он централизует чтение и запись, гарантирует, что прямое значение в __dict__ не обойдёт валидацию, и может переиспользоваться в разных моделях. Цена решения — более сложная модель поиска атрибутов и необходимость учитывать поведение при доступе через класс.

Что кандидаты часто упускают

  1. Что изменится, если у дескриптора останется только __get__?

    Он станет дескриптором без данных. При c.x сначала будет проверен c.__dict__, поэтому значение instance перекроет результат дескриптора. Такой вариант полезен, например, для ленивого вычисления, которое можно заменить значением экземпляра.

  2. Почему наличие метода __set__, который всегда выбрасывает исключение, всё равно влияет на приоритет?

    Для определения дескриптора данных важно само наличие __set__ или __delete__, а не успешность операции записи. Поэтому даже read-only-дескриптор остаётся дескриптором данных и продолжает иметь приоритет над __dict__.

  3. Что произойдёт при обращении к C.x, а не к c.x?

    Python всё равно вызывает D.__get__, но передаёт obj=None и owner=C. Дескриптор должен явно обработать этот случай. Обычно он возвращает себя, чтобы класс можно было инспектировать, либо предоставляет метаданные поля. Если этот случай не учесть, обращение через класс может завершиться ошибкой или вернуть неподходящее значение.