Метаклассы и интроспекция
В Python нет отдельного «времени компиляции классов». Оператор class — это исполняемая инструкция: интерпретатор выполняет тело класса как обычный блок кода, собирает получившиеся имена в словарь-namespace и передаёт его конструктору, который создаёт объект-класс и связывает с ним имя. Такой объект кладут в переменную, передают аргументом, собирают целиком во время выполнения. Класс класса называется метаклассом; по умолчанию это type, и вся первая половина темы выводится из одного факта: класс — это экземпляр своего метакласса, а метакласс — обычный класс, унаследованный от type.
Вторая половина — про то, как программа читает и меняет саму себя, и разграничение здесь спрашивают дословно: интроспекция читает (type, isinstance, dir, __dict__, модуль inspect), рефлексия действует — пишет атрибуты по имени-строке, вызывает методы, конструирует объекты по имени, неизвестному на момент написания кода. Ловушки стоит назвать заранее. type — это два совершенно разных вызова под одним именем. __getattr__ срабатывает только после того, как обычный поиск уже провалился, а __getattribute__ перехватывает каждое обращение и почти всегда уходит в бесконечную рекурсию с первого раза. Конфликт метаклассов возникает не из-за наследования, а из-за несовместимости метаклассов баз. И главное — после появления __init_subclass__ и __set_name__ большинство задач, ради которых раньше писали метакласс, решается без него.
Карта темы
- Классы — это объекты —
classсоздаёт объект во время выполнения;type(Foo)— это метакласс, аFoo.__bases__— совсем другая ось. - Интроспекция — чтение типа и структуры объекта на ходу через
type,isinstance,dir,__dict__иinspect. - getattr и хуки доступа к атрибутам — доступ по строковому имени, роль
defaultи разница__getattr__и__getattribute__. - Рефлексия — запись атрибутов, вызов метода по имени и импорт по строке, а также цена этой динамики.
- type — функция и конструктор классов — форма с одним аргументом возвращает тип, форма с тремя собирает новый класс.
- Метакласс — класс класса — как метакласс назначается, наследуется и почему возникает конфликт метаклассов.
- Механика создания класса — полная цепочка от
__prepare__до__init__и почему__init_subclass__вытеснил метаклассы.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать метаклассом базовый класс из скобок после имени | Ось наследования (__bases__) путается с осью создания (type(Foo)) — после этого не объяснить ни type(type), ни конфликт метаклассов |
Думать, что type(Foo) вернёт object | object — вершина наследования, а не создатель классов; type(Foo) возвращает метакласс, по умолчанию type |
Верить, что type(name, bases, ns) строит экземпляр | Три аргумента строят новый класс — ровно то, что делает оператор class внутри |
Считать, что хук метакласса срабатывает на каждый Foo() | __new__/__init__ метакласса работают один раз при определении класса; за создание экземпляров отвечает Meta.__call__ |
Полагать, что default в getattr — это значение при успехе | default подставляется только вместо AttributeError; любое другое исключение из property пролетает наружу |
Реализовать __getattribute__ через self.__dict__[name] | Обращение к self.__dict__ снова входит в __getattribute__ — RecursionError; нужен object.__getattribute__ |
| Брать метакласс для валидации или регистрации подклассов | Именно эту нишу закрывают __init_subclass__ и __set_name__ — метакласс добавляет здесь только риск конфликта |
Значение для собеседований
Тема отмечена как продвинутая, но начинают её с двух коротких вопросов, отсеивающих заученное. Первый — что такое метакласс, и правильный ответ формулируется через отношение экземпляра, а не наследования: метакласс создаёт объекты-классы так же, как класс создаёт свои экземпляры. Второй — в каком смысле класс является объектом, и здесь ждут type(Foo) is type, Foo.__class__ и умение создать класс во время выполнения. Дальше идёт type с одним и с тремя аргументами — за видимостью синтаксического вопроса проверяют понимание того, что оператор class вызывает ровно этот конструктор.
Средний уровень разговора — механика. Просят перечислить, что и в каком порядке получает метакласс, чем __new__ отличается от __init__ и когда срабатывает __init_subclass__. Здесь же спрашивают разницу getattr(obj, name) и obj.name, разницу __getattr__ и __getattribute__ и ловушку с рекурсией в перехватчике. Финальный вопрос почти всегда один — где метаклассы применяются на практике. Сильный ответ называет декларативные API фреймворков (abc.ABCMeta, enum.EnumType, ModelBase в Django) и сразу добавляет, что в прикладном коде почти всегда достаточно __init_subclass__, __set_name__ или декоратора класса. Ответ «метаклассы нужны для наследования» или «для скорости» закрывает тему в минус.