Назовите точку перехвата вызова класса, которая выполняется до его new, и объясните, где она должна быть определена.
Вызов класса перехватывает метод call метакласса. Он должен быть определён в метаклассе, потому что сам класс является объектом этого метакласса: выражение C() фактически обращается к операции вызова объекта C.
Стандартная реализация type.__call__ обычно организует жизненный цикл экземпляра: вызывает __new__, а затем при необходимости __init__. Переопределив __call__ метакласса, можно изменить этот процесс, например добавить кэширование или проверку аргументов.
В Python классы являются объектами. Поэтому для управления поведением самих классов нужен отдельный уровень объектов — метаклассы. Такой подход позволяет настраивать создание классов, доступ к их атрибутам и вызов класса без изменения каждого экземпляра отдельно.
Метод __call__ метакласса решает задачу централизованного управления выражением Класс(...). Это полезно для фабрик, синглтонов, регистрации экземпляров и других сценариев, где обычной логики __new__ и __init__ недостаточно.
Если изменить только __new__ или __init__, нельзя полностью перехватить сам вызов класса: часть стандартного жизненного цикла уже определяется реализацией type.__call__. Например, кэширование в __init__ не предотвращает создание нового объекта, а кэширование в __new__ смешивает управление экземпляром с логикой его конструирования.
Ошибка в метаклассе может привести к тому, что __init__ будет вызван несколько раз, создание экземпляра полностью пропустится или вместо экземпляра вернётся объект другого типа. Поэтому такой механизм следует применять только при необходимости и документировать его поведение.
Когда выполняется C(), Python вызывает __call__ у метакласса класса C. Для обычного класса метаклассом является type, поэтому используется type.__call__.
Упрощённо стандартная последовательность выглядит так:
C.__new__(C, ...);C, для него вызывается C.__init__(...);Если у C пользовательский метакласс, его __call__ может выполнить собственную логику и затем делегировать стандартное создание через super().__call__(*args, **kwargs).
Здесь второй вызов Service() не запускает стандартный конструктор: метакласс возвращает уже сохранённый объект. Это отличается от обычного поведения класса и может быть неожиданным для вызывающего кода.
Важно не вызывать type.__call__ без необходимости обходить пользовательскую иерархию метаклассов: обычно корректнее использовать super().__call__. Также нужно учитывать потокобезопасность кэша, жизненный цикл объектов, повторную инициализацию и возможные конфликты метаклассов при множественном наследовании.
Метод __call__, определённый у самого класса, относится к вызову его экземпляров: он влияет на instance(), но не на Class(). Чтобы перехватить именно вызов класса, __call__ размещают в его метаклассе.
В библиотеке нужно гарантировать единственный экземпляр клиента подключения для каждого набора параметров. Рассматривались три варианта: хранить кэш в фабричной функции, реализовать кэш в __new__ или использовать метакласс.
Фабричная функция проще для отладки, но вызывающий код должен помнить о необходимости использовать именно её. __new__ хорошо подходит для контроля идентичности экземпляров, однако усложняет разделение кэширования и инициализации; кроме того, __init__ может вызываться повторно при последующих вызовах класса.
Выбран метакласс с кэшем и явной синхронизацией доступа. Он централизует правило на уровне вызова класса, предотвращает повторную инициализацию и сохраняет привычный интерфейс Client(config). При этом документация отдельно фиксирует, что изменение аргументов после первого создания не переинициализирует объект.
1. Вопрос: Можно ли перехватить вызов C() методом __call__, определённым внутри класса C?
Ответ: Нет, такой метод обычно определяет поведение экземпляров C, то есть вызов c(). Сам объект C является экземпляром метакласса, поэтому для перехвата C() нужен __call__ его метакласса. Это следствие объектной модели Python: операция вызова ищется на уровне типа вызываемого объекта, а не в его обычном пространстве имён экземпляра.
2. Вопрос: Что произойдёт, если пользовательский __call__ метакласса не делегирует выполнение super().__call__?
Ответ: Стандартное создание может вообще не произойти. Метод может вернуть существующий объект, значение-заглушку или выбросить исключение; __new__ и __init__ класса тогда не будут вызваны автоматически. Такой контроль допустим, но контракт вызова класса меняется, поэтому результат и побочные эффекты должны быть явно определены.
3. Вопрос: Обязан ли __call__ метакласса возвращать экземпляр вызываемого класса?
Ответ: Нет. Пользовательский __call__ может вернуть произвольный объект, поскольку Python не выполняет за него обязательную проверку результата. Однако если он делегирует в type.__call__, стандартная логика учитывает результат __new__: __init__ вызывается только когда возвращённый объект является экземпляром создаваемого класса. Возврат объекта другого типа допустим технически, но обычно нарушает ожидания пользователей API и должен иметь вескую причину.