Вызываемый объект хранится атрибутом класса и реализует __get__: почему обращение к нему через экземпляр может автоматически добавить экземпляр в вызов?
Потому что такой объект становится дескриптором. При доступе через экземпляр Python вызывает его __get__, а реализация этого метода может вернуть связанный вызываемый объект, который передаст экземпляр первым аргументом.
Само наличие __call__ автоматическую передачу экземпляра не обеспечивает. Для этого нужен именно протокол дескрипторов и корректная реализация __get__.
Дескрипторы — часть объектной модели Python, предназначенная для унифицированного управления доступом к атрибутам. На этом механизме основаны методы классов, свойства и другие конструкции, которым нужно изменять поведение обычного чтения атрибута.
Благодаря дескрипторам один и тот же атрибут класса может по-разному вести себя при доступе через класс и через экземпляр. Это позволяет отделить хранение объекта от логики его связывания с владельцем.
Обычный вызываемый объект, помещённый в атрибут класса, не обязан получать экземпляр автоматически. Если он не реализует __get__, обращение через экземпляр обычно возвращает тот же объект, поэтому вызов не получает self как дополнительный аргумент.
Если добавить __get__, но неверно реализовать его, можно получить лишний аргумент, потерять доступ к экземпляру или неожиданно разрешить перекрытие атрибута значением из __dict__ экземпляра. Поэтому важно учитывать не только вызываемость объекта, но и правила поиска атрибутов.
При обращении к атрибуту Python проверяет класс и его базовые классы. Если найденный объект имеет метод __get__, он рассматривается как дескриптор, и вызывается примерно с двумя параметрами: экземпляром и классом.
При доступе через класс экземпляр равен None. При доступе через экземпляр дескриптор получает сам экземпляр и может вернуть обёртку, связанную с ним. В такой обёртке экземпляр становится первым аргументом последующего вызова.
MethodType создаёт связанный вызываемый объект: при вызове Service().handle(1) объект Service передаётся первым аргументом в Handler.__call__.
Такой дескриптор является неуправляющим, потому что определяет только __get__. Значение с тем же именем в словаре экземпляра может его перекрыть. Если дескриптор дополнительно определяет __set__ или __delete__, он становится управляющим, и правила доступа меняются.
Важно отличать два случая: атрибут класса участвует в дескрипторном поиске, а объект, записанный непосредственно в атрибут экземпляра, обычно возвращается как есть. Поэтому перенос такого объекта с класса в экземпляр может убрать автоматическое связывание.
В библиотеке обработки запросов один объект-обработчик нужно объявить атрибутом класса сервиса, но при вызове ему требуется конкретный экземпляр сервиса: настройки, клиент базы данных и журналирование.
Вариант с обычным вызываемым объектом прост, но экземпляр сервиса не передаётся автоматически. Замыкание может захватить экземпляр, однако тогда его нужно создавать отдельно для каждого сервиса, а жизненный цикл и отладка состояния становятся менее прозрачными. functools.partial удобен для фиксированных аргументов, но сам по себе не превращает объект в дескриптор.
Выбран дескриптор с __get__, возвращающий связанный объект. Это сохраняет декларативное объявление обработчика в классе и обеспечивает доступ к экземпляру при вызове. Компромисс — дополнительная сложность протокола и необходимость явно решить, должен ли обработчик быть общим для всех экземпляров или создавать отдельное состояние.
1. Что вернёт дескриптор при обращении к атрибуту через сам класс?
Обычно __get__ получает instance=None. Хорошая реализация в этом случае возвращает сам дескриптор, чтобы его можно было инспектировать, переиспользовать или вызвать в классовом контексте. Возврат связанного объекта и при таком доступе может сделать API непредсказуемым.
2. Достаточно ли определить у вызываемого объекта только __call__, чтобы он получил экземпляр класса?
Нет. __call__ отвечает лишь за возможность вызвать объект. Автоматическое связывание выполняется дескрипторным протоколом через __get__; без него объект класса не получает специальной обработки только потому, что он вызываемый.
3. Может ли экземпляр перекрыть такой дескриптор собственным атрибутом?
Если дескриптор определяет только __get__, да. Такой дескриптор неуправляющий, поэтому значение в instance.__dict__ обычно имеет приоритет при доступе через экземпляр. Дескриптор с __set__ или __delete__ является управляющим и получает приоритет над обычным атрибутом экземпляра.