Программирование PythonМодель данныхPython-разработчик, создающий расширяемые библиотеки и системы плагинов

В какой момент Python вызывает init subclass при создании нового класса наследника?

В какой момент Python вызывает __init_subclass__ при создании нового класса-наследника?

Проходите собеседования с ИИ помощником Hintsage

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

Python вызывает __init_subclass__ после создания нового класса и до применения декораторов к этому классу. Метод вызывается у непосредственного базового класса, а сам новый класс передаётся ему как cls.

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

Хук __init_subclass__ появился в Python 3.6 вместе с PEP 487. Он предназначен для настройки подклассов без создания отдельной метакласса: базовый класс может автоматически регистрировать наследников, проверять их атрибуты или обрабатывать параметры объявления класса.

До этого подобные задачи часто решали через метаклассы. Метаклассы мощнее, но могут конфликтовать при множественном наследовании и усложнять архитектуру.

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

Базовому классу иногда нужно реагировать на появление каждого наследника: добавить его в реестр плагинов, проверить обязательные атрибуты или сохранить параметры конфигурации. Если выполнять такую логику вручную, легко забыть зарегистрировать новый класс или получить различия между разными способами его создания.

Важно не путать __init_subclass__ с __init__: он не инициализирует экземпляр, а выполняется один раз при создании самого класса-наследника. Ошибка в этом хуке возникает уже во время определения класса, поэтому модуль с таким классом может не импортироваться.

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

При выполнении объявления класса Python сначала исполняет тело класса и формирует пространство его атрибутов. Затем создаётся объект-класс; в процессе создания обрабатываются дескрипторы с __set_name__, после чего вызывается __init_subclass__ непосредственного родителя.

Хук получает новый класс в параметре cls. Именованные параметры, указанные в объявлении класса, могут быть переданы в __init_subclass__; при переопределении обычно следует передавать оставшиеся параметры через super(), чтобы не нарушить кооперативное множественное наследование.

class Plugin: registry = [] def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) Plugin.registry.append(cls) class JsonPlugin(Plugin, format='json'): pass print(Plugin.registry == [JsonPlugin])

В примере __init_subclass__ вызывается во время создания JsonPlugin, поэтому класс попадает в реестр ещё до выполнения следующего оператора после объявления класса. Параметр format передаётся хуку; вызов super() важен, если в цепочке наследования есть другие реализации этого метода.

Хук вызывается для каждого прямого наследования от класса, в котором он определён или унаследован. Сам базовый класс при своём объявлении этот хук для себя не вызывает. Декораторы класса применяются позже, поэтому __init_subclass__ видит исходный объект класса, а не результат работы декоратора.

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

В системе плагинов каждый класс должен автоматически появляться в реестре при импорте модуля. Рассматривались ручная регистрация, декоратор и метакласс.

Ручная регистрация проста, но зависит от дисциплины разработчика: новый плагин можно забыть добавить. Декоратор делает регистрацию явной, однако требует не забывать декоратор и выполняется уже после __init_subclass__. Метакласс позволяет централизовать больше правил, но повышает сложность и может конфликтовать с метаклассами сторонних базовых классов.

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

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

  1. Вызывается ли __init_subclass__ у всех предков?

Нет, Python напрямую вызывает хук непосредственного базового класса нового наследника. Реализации других базовых классов могут быть вызваны только косвенно, если текущая реализация корректно вызывает super().__init_subclass__() и цепочка наследования построена кооперативно.

  1. Что произойдёт с именованными параметрами объявления класса?

Они могут быть переданы в __init_subclass__ как ключевые аргументы. Реализация должна либо обработать допустимые параметры и удалить их из дальнейшей передачи, либо передать оставшиеся параметры через super(). Если конечная реализация получает неподдерживаемый аргумент, обычно возникает TypeError; поэтому сигнатуры хуков в иерархии нужно согласовывать.

  1. Может ли декоратор класса изменить результат работы __init_subclass__?

Декоратор применяется после создания класса и после вызова __init_subclass__. Поэтому хук может зарегистрировать исходный класс, а декоратор затем вернуть другой объект или изменить атрибуты класса. Если реестр должен хранить именно окончательный результат декорирования, одной регистрации через __init_subclass__ недостаточно — потребуется дополнительная логика в декораторе или другой этап настройки.