Сравните приоритет доступа к атрибуту класса: что победит — дескриптор в метаклассе или одноимённый атрибут самого класса?
Дескриптор данных в метаклассе побеждает одноимённый атрибут в словаре класса. Если дескриптор в метаклассе не является дескриптором данных, атрибут самого класса имеет приоритет.
Иными словами, при обращении к C.x Python сначала проверяет метакласс type(C), но безусловно перехватывает доступ только дескриптор метакласса с __set__ или __delete__.
Дескрипторы были введены как общий механизм управления доступом к атрибутам. На них основаны методы экземпляров, свойства, многие элементы ORM и другие конструкции, которым нужно выполнять логику при чтении или записи атрибута.
Метаклассы расширяют тот же механизм на сами классы: экземпляром метакласса является объект-класс. Поэтому атрибут класса может находиться не только в словаре класса, но и разрешаться через метакласс.
Класс в Python сам является объектом. Поэтому выражение C.x — это не просто чтение C.__dict__['x'], а поиск атрибута у объекта C с учётом правил его метакласса.
Неверное предположение, что атрибут в словаре класса всегда важнее метакласса, приводит к неожиданному поведению: метакласс может незаметно перехватить чтение, запись или удаление атрибута. Это особенно рискованно в инфраструктурном коде, например в ORM, системах регистрации классов и API с декларативными настройками.
Для C.x Python рассматривает C как экземпляр метакласса Meta. Упрощённый порядок поиска такой:
__get__, и атрибут класса C.__dict__['x'] не используется.C.Дескриптор считается дескриптором данных, если его тип определяет __set__ или __delete__. Одного __get__ недостаточно: такой дескриптор является недескриптором данных, поэтому одноимённый атрибут класса его перекрывает.
Здесь C.x вызывает Meta.x.__get__: объект C является экземпляром Meta. Если удалить __set__ из MetaDescriptor, дескриптор перестанет быть дескриптором данных, и результатом станет значение C.x из словаря класса.
Это правило относится именно к доступу через класс. При доступе к атрибуту экземпляра действуют другие уровни поиска, а дескрипторы самого класса и дескрипторы метакласса не являются взаимозаменяемыми.
В ORM метакласс может предоставлять классу служебный атрибут objects, который должен всегда возвращать менеджер, связанный с конкретной моделью. Разработчик одновременно объявил в модели поле с тем же именем и ожидал, что оно будет доступно как обычный атрибут.
Вариант с дескриптором данных в метаклассе надёжно сохраняет инфраструктурное поведение, но скрывает ошибочное совпадение имён. Вариант с недескриптором позволяет полю модели перекрыть служебный атрибут, однако делает API зависимым от порядка и места объявления.
Практичнее выбрать дескриптор данных и дополнительно проверять конфликт имён при создании класса. Тогда системный атрибут не будет случайно заменён, а ошибка проявится на этапе определения модели, а не во время выполнения запроса.
1. Какое значение получает obj в __get__ дескриптора метакласса при обращении через класс?
obj будет равен самому классу C, а owner — его метаклассу Meta. Это отличается от обращения к дескриптору класса через экземпляр, где obj обычно является экземпляром обычного класса.
Причина в том, что C — объект, для которого выполняется поиск атрибута, а Meta — тип этого объекта. Поэтому метаклассический дескриптор может использовать obj для работы с конкретным классом.
2. Может ли дескриптор только с __get__, находящийся в метаклассе, перекрыть атрибут класса?
Нет. Такой дескриптор является недескриптором данных. Если в словаре C есть одноимённый атрибут, Python выберет его; метаклассовый дескриптор будет использован только после отсутствия атрибута в самом классе.
Это важное отличие от дескриптора данных: наличие __set__ или __delete__ меняет приоритет чтения, даже если запись фактически не поддерживается и метод только выбрасывает исключение.
3. Что происходит при присваивании C.x, если в метаклассе есть дескриптор данных с таким именем?
Python вызывает __set__ этого дескриптора. Запись в C.__dict__ не выполняется, поэтому дескриптор может валидировать значение, сохранять его отдельно или запретить изменение.
Если подходящего дескриптора данных в метаклассе нет, присваивание атрибуту класса обычно изменяет словарь самого класса C, а не словарь метакласса. Это отдельный путь от чтения и требует учитывать, что чтение после присваивания снова подчиняется приоритету дескрипторов.