Вопрос с собеседований про то, что вернёт type от класса, ломает больше карьер, чем кажется. Интуиция шепчет про объект, правильный ответ называет метакласс, и разговор скатывается в туман. Туман не безобиден: метаклассы работают в каждом втором фреймворке, от абстрактных базовых классов до моделей ORM, но прикладывают к ним руки в основном библиотечные авторы, и то после того, как перепробовали хуки попроще. Ниже разбор механики без общих слов: что происходит при исполнении инструкции class, зачем метаклассу методы new и call, из-за чего вспыхивает знаменитый конфликт метаклассов, чем закрыли типовые нужды хуки из Python 3.6, и где проходит граница между оправданным перехватом и усложнением, которое не окупается.
Что на самом деле делает встроенный type при исполнении инструкции class
Инструкция class это синтаксический сахар над вызовом. При исполнении модуля интерпретатор собирает тело класса в словарь, затем вызывает type с тремя аргументами: именем, кортежем базовых классов и словарём атрибутов. Результат вызова, новый объект класса, привязывается к имени. То же самое проделывается руками без всякой инструкции:
def make_greeter(name):
def greet(self):
return f"привет от {self.name}"
return type(name, (object,), {"name": name, "greet": greet})
Greeter = make_greeter("Greeter")
print(Greeter().greet())
Класс, собранный функцией, неотличим от записанного декларативно: те же методы, тот же isinstance, тот же синтаксис подклассов. Раз type создаёт классы, возникает вопрос, кто создаёт сам type, и ответ закольцован: type является собственным метаклассом, type(type) возвращает type. Отсюда определение без тумана: метакласс это класс, экземплярами которого служат другие классы. У любого класса можно спросить хозяина, и type(Spam) покажет, кто собирал Spam.
Полный конвейер рождения класса состоит из трёх шагов. Сначала у метакласса вызывается метод prepare, который возвращает хранилище для локальных имён тела класса, по умолчанию обычный словарь. Затем исполняется само тело, и все присваивания попадают в это хранилище. В конце type.new собирает объект класса, а type.init настраивает его. Обычное объявление проходит конвейер молча, метакласc получает право вмешаться в каждый шаг. Класс и сам остаётся обычным объектом: его можно положить в словарь, передать в функцию и опросить атрибуты, и это роднит метапрограммирование с повседневным кодом.
Как собственный метакласс перехватывает рождение класса через new и call
Метакласc пишется как наследник type, и у него есть две точки перехвата. Метод new получает имя, базы и словарь атрибутов до создания объекта класса: здесь можно досмотреть словарь, дописать, провалидировать и изменить. Метод init вызывается после создания и служит настройкой. Типовая задача для new, регистрация плагинов по псевдониму, решается без единой строки в теле самих плагинов:
class CommandMeta(type):
def __new__(mcls, name, bases, namespace):
cls = super().__new__(mcls, name, bases, namespace)
if hasattr(cls, "registry") and "alias" in namespace:
cls.registry[namespace["alias"]] = cls
return cls
class Command(metaclass=CommandMeta):
registry = {}
class Ping(Command):
alias = "ping"
def run(self):
return "pong"
handler = Command.registry["ping"]()
Метод new подходит и для проверок тела класса. Каркас требует, чтобы каждый обработчик объявлял метод run, и ошибка в объявлении должна ловиться при импорте, а не при первом вызове:
class CommandMeta(type):
def __new__(mcls, name, bases, namespace):
if bases and "run" not in namespace:
raise TypeError(f"у команды {name} нет метода run")
return super().__new__(mcls, name, bases, namespace)
Проверка срабатывает на этапе сборки, и опечатка в имени метода обнаруживается при старте приложения, а не глубокой ночью в работающем сервисе.
Вторая точка мощнее, и о ней забывают чаще всего: метод call метакласса вызывается при каждом создании экземпляра. Обычный класс получает его от type, и он просто строит объект через new и init. Перехватив call, класс контролирует свои экземпляры целиком, так собирают одиночек, пулы и кэширующие фабрики:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class AppConfig(metaclass=SingletonMeta):
pass
assert AppConfig() is AppConfig()
Правила выбора метакласса при наследовании и знаменитая ошибка metaclass conflict
У класса может быть только один метакласс. При создании нового класса интерпретатор вычисляет его по строгому правилу: собирает метаклассы всех баз и явно указанный, победителем становится самый производный, то есть наследующий остальных кандидатов. Если два кандидата не в родстве, импорт падает с TypeError и словами metaclass conflict, и падение случается в самый неподходящий момент:
class MetaA(type):
pass
class MetaB(type):
pass
class BaseA(metaclass=MetaA):
pass
class BaseB(metaclass=MetaB):
pass
class Combined(BaseA, BaseB):
pass
Лечится конфликт созданием общего наследника двух метаклассов и явным указанием его в сборке:
class MetaAB(MetaA, MetaB):
pass
class Combined(BaseA, BaseB, metaclass=MetaAB):
pass
Именно из-за этой хрупкости PEP 487 в мотивации прямо назвал цену: метакласс, повешенный на класс, живёт с ним весь жизненный цикл и плодит конфликты при смешивании библиотек в одном наследовании. Обиднее всего, что вспыхивает конфликт не там, где метакласс написали, а там, где два чужих класса встретились в третьем проекте, и автор библиотеки узнаёт об этом из чужого трейсбека.
Чем унаследованный метакласс отличается от декоратора при росте иерархии
У декоратора класса есть коварное свойство, из-за которого регистрация на нём регулярно ломается: он применяется ровно к одному классу и один раз. Подклассы декорированного класса ничего не наследуют от обёртки. Декоратор, вписавший Ping в реестр, молча пропустит CachedPing, объявленный как наследник, и реестр останется неполным, пока кто-нибудь не заметит пропажу в рантайме. Метакласс и хук __init_subclass__ работают наоборот: они присягают всей иерархии. Каждый новый подкласс проходит через них автоматически, потому что создание класса это вызов метакласса, а метакласс наследуется от базовых классов так же, как обычные атрибуты.
Разница ощущается и в обратную сторону. Декоратор невидим для будущих подклассов и легко снимается с одного класса, метакласс обязателен для всех потомков и отказывает только всей иерархией сразу. Выбор между ними это выбор политики: разовая пометка одного класса или правило для всех, кто от него родится. Практичный вывод для каркасов: если нужно, чтобы авторы плагинов не могли забыть о регистрации, работает только наследуемый механизм, и здесь у метакласса и __init_subclass__ конкурентов нет.
Что закрыли хуки init subclass и set name в Python 3.6
До версии 3.6 почти любой из перечисленных сценариев заканчивался метаклассом, и экосистема покрылась ими, как город проводами. PEP 487, принятый в 2016 году, добавил два хука, закрывшие большинство задач без перехвата. Первый, __init_subclass__, объявляется в родительском классе и вызывается при создании каждого подкласса, автоматически работая как класс-метод:
class Command:
registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if "alias" in cls.__dict__:
cls.registry[cls.alias] = cls
Та же регистрация, четыре строки, ноль новых сущностей и наследование без сюрпризов. Второй хук, __set_name__, передаёт дескрипторам знание о себе: в момент создания класса каждый дескриптор получает владельца и имя, под которым записан:
import weakref
class WeakRef:
def __set__(self, instance, value):
instance.__dict__[self.attr] = weakref.ref(value)
def __set_name__(self, owner, name):
self.attr = name
class TreeNode:
parent = WeakRef()
def __init__(self, parent):
self.parent = parent
До этого хука дескриптор узнавал своё имя только при первом обращении, и слабые ссылки на родителя приходилось конфигурировать вручную. Третья категория, порядок появления атрибутов, закрылась соседним PEP 520, после чего словарь класса стал честно хранить очерёдность записи. Вывод из реформы простой: если задача закрывается хуками, метакласс не пишут вовсе, и большинство команд за эти годы вообще перестали писать свои.
Где метаклассы живут по-настоящему от абстрактных баз до моделей
Настоящая работа метаклассов происходит там, где хуков принципиально не хватает. ABCMeta из модуля abc держит на метаклассе проверку instancecheck, поэтому isinstance умеет признавать совместимым класс с нужной структурой, а абстрактные методы проверяются при создании экземпляра: объект с незакрытыми абстракциями падает с TypeError прямо в конструкторе.
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def load(self, key):
...
Storage()
Метакласс Enum превращает присваивания в теле класса в члены перечисления, сами по себе значения полей никогда не существуют отдельно от класса. Декларативные модели в Django, SQLAlchemy и pydantic собираются метаклассами, которые проходят по атрибутам и аннотациям, достают описания полей из нескольких баз, строят схему и готовят валидацию к первому использованию: класс объявляется один раз, а его строение анализируется при импорте. Для слияния описаний из нескольких баз у метакласса есть метод prepare, возвращающий заказанное хранилище вместо обычного словаря, на этом держатся библиотеки признаков, смешивающие поведение из многих источников. Общее у всех примеров одно: перехват нужен на этапе рождения класса, до того как объект начал существовать, и никакой хук этого места не достигает.
Практические правила когда метакласс оправдан а когда он вредит читателю кода
Разобраться в чужом метаклассе помогает его же природа: это обычный класс с обычными исходниками, чьи методы можно вызывать руками, а type(YourClass).__mro__ покажет всю родословную сборщика. Статические анализаторы понимают метаклассы ограниченно: поведение, вычисляемое при создании класса, для них невидимо, и это ещё один аргумент держать перехват тонким и покрытым тестами.
Соблазн унаследоваться от type проверяется коротким списком, который стоит держать под рукой:
- Убедиться, что задача не решается декоратором класса: регистрация, валидация и обёртки ставятся им без следа в иерархии наследования;
- Для реакции на появление подклассов достаточно хука __init_subclass__, а для дескрипторов, узнавших своё имя, есть __set_name__;
- Метакласс оправдан, когда меняется сам процесс сборки: словарь класса до создания, слияние описаний из нескольких баз, контроль рождения экземпляров через call;
- Библиотечный метакласс обязан быть минимальным и комбинируемым, иначе каждое множественное наследование с чужим классом обернётся конфликтом;
- Поведение, невидимое в теле класса, покрывается тестами и документацией, потому что читатель не обязан угадывать, отчего подклассы регистрируются сами;
- Если метакласс пишется внутри приложения, внутри приложения почти всегда прячется маленькая библиотека, и её границы стоит провести явно.
Метакласс превращается из инструмента в проблему там, где его применяют ради красоты решения или по незнанию хуков, появившихся почти десять лет назад. Порядок зрелости обратной силы не имеет: декоратор, потом хуки, потом метакласс, и лишь последний шаг даёт доступ к моменту рождения класса. Собеседования правы в одном: вопрос про type от класса проверяет не знание экзотики, а понимание, что классы это объекты, собираемые во время импорта. Кто хоть раз собрал класс тремя аргументами type руками, тот перестаёт видеть в инструкции class чёрный ящик и начинает видеть вызов, а чужие метаклассы читает как обычный код с методами new, init и call.