Что произойдёт при вызове экземпляра, если __call__ задан только как атрибут этого экземпляра, но не определён в его классе?
Такой экземпляр не станет вызываемым: попытка вызова завершится исключением TypeError, несмотря на наличие атрибута __call__ в самом объекте. Для определения вызываемости Python ищет специальный метод __call__ через тип экземпляра, а не через его обычные атрибуты.
Присваивание __call__ классу, напротив, делает его экземпляры вызываемыми, поскольку специальный метод будет обнаружен в структуре типа.
В Python специальные методы образуют единый протокол взаимодействия объектов с операторами и встроенными конструкциями языка. К ним относятся, например, __len__, __iter__, __enter__ и __call__.
Такой подход позволяет вызывать экземпляры пользовательских классов так же, как функции, но сохраняет предсказуемые правила поиска специальных методов. Вызов объекта относится к операциям самого типа, поэтому Python не рассматривает произвольный атрибут экземпляра как замену методу протокола.
Разработчик может попытаться добавить функцию в атрибут __call__ уже созданного экземпляра и ожидать, что объект станет вызываемым. Это особенно вероятно при динамической настройке обработчиков, декораторов или объектов-конфигураций.
Неверное предположение приводит к ошибке только в момент вызова. Дополнительная проблема состоит в том, что проверка callable также покажет False, поэтому объект нельзя надёжно адаптировать к протоколу вызова через индивидуальный атрибут экземпляра.
При операции вызова Python проверяет, поддерживает ли тип объекта протокол вызова. На уровне реализации это связано со специальным слотом типа, который заполняется при наличии __call__ у класса. Обычный поиск атрибута через словарь экземпляра для этой операции не используется.
Поэтому атрибут instance.__call__ может существовать и быть обычным вызываемым значением, но синтаксис вызова instance() его не применит. Это отличается от явного обращения instance.__call__(), которое может сработать как обычный доступ к атрибуту.
Правильный способ создать вызываемый объект — определить __call__ в классе. Такой объект часто удобнее замыкания, когда состояние должно быть явно представлено полями, а поведение — методом.
Есть важное ограничение: специальные методы обычно следует определять на уровне класса, а не динамически на отдельных экземплярах. Изменение класса может обновить механизм специальных методов для его экземпляров, но полагаться на присваивание атрибутов конкретному объекту нельзя.
В системе маршрутизации разработчик хотел индивидуально добавить обработчик каждому объекту, присваивая ему функцию через атрибут __call__. Внутренняя проверка принимала объект как обработчик, но фактический вызов завершался TypeError.
Рассматривались два варианта. Первый — вызывать сохранённый атрибут явно; это минимально меняло код, но нарушало обычный контракт вызываемого объекта и требовало специальной логики у всех потребителей. Второй — создать класс-адаптер с корректным __call__; он добавлял небольшой слой, зато позволял использовать стандартную проверку callable и обычный синтаксис вызова.
Выбран был адаптер с методом __call__. В результате маршрутизатор работал с единым протоколом, а состояние обработчика хранилось в полях экземпляра; это также упростило тестирование и диагностику.
Сработает ли явный вызов атрибута __call__, записанного в экземпляр?
Да, если значение атрибута само является вызываемым объектом: обращение к instance.__call__() — это обычный поиск атрибута экземпляра. Но это не меняет результат callable(instance) и не делает допустимым синтаксис instance().
Достаточно ли определить __call__ динамически у класса после создания экземпляров?
Для обычных пользовательских классов добавление метода в класс делает его экземпляры вызываемыми, поскольку специальный метод относится к типу. Однако такой приём ухудшает предсказуемость программы и обычно уступает явному определению метода в теле класса или использованию отдельного адаптера.
Почему объект класса вызываем, даже если у пользовательского класса нет __call__?
Сам класс является экземпляром метакласса, обычно type. У type есть механизм создания экземпляров, поэтому выражение с именем класса вызывает type.__call__, который организует создание объекта. Это отдельный случай: вызываемость самого класса не означает, что каждый его экземпляр автоматически вызываем.