Подтвердить что ты не робот

Почему динамическое добавление метода `__call__` к экземпляру не работает?

В обоих Python 2 и Python 3 код:

class Foo(object):
    pass

f = Foo()
f.__call__ = lambda *args : args

f(1, 2, 3)

возвращается как ошибка Foo object is not callable. Почему это происходит?

PS: В классах старого стиля он работает как ожидалось.

PPS: Это поведение предназначено (см. принятый ответ). В качестве рабочего решения можно определить __call__ на уровне класса, который просто пересылается другому члену и устанавливает этот "нормальный" член в реализацию для каждого экземпляра __call__.

4b9b3361

Ответ 1

Методы Double-underscore всегда ищут класс, а не экземпляр. См. Специальный поиск методов для классов нового стиля:

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

Это потому, что типу, возможно, потребуется поддерживать ту же операцию (в этом случае специальный метод просматривается по метатипу).

Например, классы вызываются (как вы создаете экземпляр), но если Python искал метод __call__ для фактического объекта, то вы никогда не сможете сделать это на классах, которые реализуют __call__ для своих экземпляров. ClassObject() станет ClassObject.__call__(), который потерпит неудачу, потому что параметр self не передается в несвязанный метод. Поэтому вместо type(ClassObject).__call__(ClassObject) используется, а вызов instance() переводится на type(instance).__call__(instance).

Ответ 2

В новых классах стиля (по умолчанию 3.x) и унаследованных от объекта в 2.x методы перехвата атрибутов __getattr__ и __getattribute__ больше не вызывается для встроенных операций на dunder перегруженных методах экземпляров, и вместо этого поиск начинается с класса.

Обоснование этого связано с техникой, введенной присутствием MetaClasses.

Поскольку классы являются экземплярами метаклассов и потому что метаклассы могут определять определенные операции, которые действуют на классы, пропуск класса (который в этом случае может считаться instance) имеет смысл; вам нужно вызвать метод, определенный в метаклассе, который определяется для процессов классов (cls в качестве первого аргумента). Если был использован поиск экземпляра, в look-up будет использоваться метод класса, который определен для экземпляров этого класса (self).

Другая (спорная причина) включает в себя оптимизацию: поскольку встроенные операции с экземплярами обычно вызывают очень часто, пропуская просмотр экземпляра в целом и переходя непосредственно в класс, мы некоторое время сохраняем. [Source Lutz, Learning Python, 5th Edition]


Основная область , где это может быть неудобно, - это создание прокси-объектов, которые перегружают __getattr__ и __getattribute__ с намерением переадресовывать вызовы во встроенный внутренний объект. Поскольку встроенный вызов будет пропускать экземпляр вообще, они не будут пойманы, и поскольку эффект не будет перенаправлен во встроенный объект.

Простая, но утомительная работа для этого - фактически перегрузить все проходы, которые вы хотите перехватить в прокси-объекте. Затем в этих перегруженных dunders вы можете делегировать вызов внутреннему объекту по мере необходимости.


Простейшая работа, о которой я могу думать, заключается в установке атрибута в классе Foo с помощью setattr:

setattr(Foo, '__call__', lambda *args: print(args))

f(1, 2, 3)
(<__main__.Foo object at 0x7f40640faa90>, 1, 2, 3)