Что произойдёт при вызове экземпляра, если call задан только как атрибут этого экземпляра, но не определён ...

Что произойдёт при вызове экземпляра, если __call__ задан только как атрибут этого экземпляра, но не определён в его классе?

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

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

Такой экземпляр не станет вызываемым: попытка вызова завершится исключением TypeError, несмотря на наличие атрибута __call__ в самом объекте. Для определения вызываемости Python ищет специальный метод __call__ через тип экземпляра, а не через его обычные атрибуты.

class Handler: pass handler = Handler() handler.__call__ = lambda: "готово" print(callable(handler)) # False handler() # TypeError: 'Handler' object is not callable

Присваивание __call__ классу, напротив, делает его экземпляры вызываемыми, поскольку специальный метод будет обнаружен в структуре типа.

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

В Python специальные методы образуют единый протокол взаимодействия объектов с операторами и встроенными конструкциями языка. К ним относятся, например, __len__, __iter__, __enter__ и __call__.

Такой подход позволяет вызывать экземпляры пользовательских классов так же, как функции, но сохраняет предсказуемые правила поиска специальных методов. Вызов объекта относится к операциям самого типа, поэтому Python не рассматривает произвольный атрибут экземпляра как замену методу протокола.

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

Разработчик может попытаться добавить функцию в атрибут __call__ уже созданного экземпляра и ожидать, что объект станет вызываемым. Это особенно вероятно при динамической настройке обработчиков, декораторов или объектов-конфигураций.

Неверное предположение приводит к ошибке только в момент вызова. Дополнительная проблема состоит в том, что проверка callable также покажет False, поэтому объект нельзя надёжно адаптировать к протоколу вызова через индивидуальный атрибут экземпляра.

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

При операции вызова Python проверяет, поддерживает ли тип объекта протокол вызова. На уровне реализации это связано со специальным слотом типа, который заполняется при наличии __call__ у класса. Обычный поиск атрибута через словарь экземпляра для этой операции не используется.

Поэтому атрибут instance.__call__ может существовать и быть обычным вызываемым значением, но синтаксис вызова instance() его не применит. Это отличается от явного обращения instance.__call__(), которое может сработать как обычный доступ к атрибуту.

Правильный способ создать вызываемый объект — определить __call__ в классе. Такой объект часто удобнее замыкания, когда состояние должно быть явно представлено полями, а поведение — методом.

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

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

В системе маршрутизации разработчик хотел индивидуально добавить обработчик каждому объекту, присваивая ему функцию через атрибут __call__. Внутренняя проверка принимала объект как обработчик, но фактический вызов завершался TypeError.

Рассматривались два варианта. Первый — вызывать сохранённый атрибут явно; это минимально меняло код, но нарушало обычный контракт вызываемого объекта и требовало специальной логики у всех потребителей. Второй — создать класс-адаптер с корректным __call__; он добавлял небольшой слой, зато позволял использовать стандартную проверку callable и обычный синтаксис вызова.

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

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

  1. Сработает ли явный вызов атрибута __call__, записанного в экземпляр?

    Да, если значение атрибута само является вызываемым объектом: обращение к instance.__call__() — это обычный поиск атрибута экземпляра. Но это не меняет результат callable(instance) и не делает допустимым синтаксис instance().

  2. Достаточно ли определить __call__ динамически у класса после создания экземпляров?

    Для обычных пользовательских классов добавление метода в класс делает его экземпляры вызываемыми, поскольку специальный метод относится к типу. Однако такой приём ухудшает предсказуемость программы и обычно уступает явному определению метода в теле класса или использованию отдельного адаптера.

  3. Почему объект класса вызываем, даже если у пользовательского класса нет __call__?

    Сам класс является экземпляром метакласса, обычно type. У type есть механизм создания экземпляров, поэтому выражение с именем класса вызывает type.__call__, который организует создание объекта. Это отдельный случай: вызываемость самого класса не означает, что каждый его экземпляр автоматически вызываем.