Setattr для метода `__call__` объекта не работает в Python

Я думал, что для объекта Python obj() эквивалентно obj.__call__(). Но похоже, что он не действует при использовании setattr.

In [46]: class A:
    ...:     def __call__(self):
    ...:         return 1
    ...:

In [47]: a = A()

In [48]: a()
Out[48]: 1

In [49]: a.__call__()
Out[49]: 1

In [50]: getattr(a, '__call__')
Out[50]: <bound method A.__call__ of <__main__.A object at 0x10a3d2a00>>

In [51]: setattr(a, '__call__', lambda: 100)

In [52]: a()
Out[52]: 1

In [53]: a.__call__()
Out[53]: 100

Почему это так? И как я могу установить метод вызова во время выполнения?

Обратите внимание, что в версии Python — 3.9.18 и 3.11.4.

Я ожидал, что фрагмент кода выше a() вернет 100

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
0
58
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Цитирую документацию, курсив мой:

object.__call__(self[, args...])

Вызывается, когда экземпляр «вызывается» как функция; если этот метод определен, x(arg1, arg2, ...) примерно переводится как type(x).__call__(x, arg1, ...).

IOW, новый набор __call__ для экземпляра объекта не будет учитываться (и setattr здесь не играет роли; вы получите то же самое поведение с a.__call__ = ...).

Ответ принят как подходящий

Проблема в том, что __call__ определяется на уровне класса, а не на уровне экземпляра. См. документацию (из Python 2, когда она была представлена) здесь.

Вам нужно обязательно определить функцию вызова на уровне экземпляра и применить к ней setattr:

class MyClass:
    def __call__(self, *args, **kwargs): 
        return self._call(self, *args, **kwargs)

>>> a = MyClass()
>>> setattr(a, '_call', lambda self: 100)
>>> a()
100

Или вы можете исправить сам класс, если хотите, чтобы каждый экземпляр MyClass() имел одну и ту же функцию __call__. Однако подумайте внимательно, хотите ли вы это сделать:

>>> setattr(MyClass, __call__, lambda self: "foo")
>>> a()
"foo"

Объяснение: Приоритет определения класса __call__ над любым определением, специфичным для экземпляра, является общим для различных имен специальных методов/перегрузок операторов в модели данных Python .

Это произошло, когда в Python 2 были введены классы нового стиля. Процитируем «порядок приоритета», введенный в (PEP 252):

Существует конфликт между именами, хранящимися в экземпляре dict, и именами, хранящимися в типе dict. Если в обоих словарях есть запись с одним и тем же ключом, какой из них нам следует вернуть? [...]

  1. Посмотрите тип dict. Если вы нашли дескриптор данных, используйте его метод get() для получения результата. Это заботится о специальных атрибутах, таких как dict и class.
  2. Посмотрите в экземпляре dict. Если вы что-нибудь найдете, то это все. (Это учитывает требование, согласно которому обычно словарь экземпляра переопределяет словарь класса.)

«Тип dict» относится к определениям уровня класса, таким как определение __call__ MyClass, а «экземпляр dict» относится к значениям, которые мы установили для конкретного экземпляра MyClass, например a выше.

В вашем вопросе исходное определение __call__ исходит из шага 1 (тип dict), в то время как любые значения, которые вы устанавливаете таким образом, устанавливаются в экземпляре dict, который имеет более низкий приоритет:

setattr(a, '__call__', lambda self: 100) 
a.__call__ = lambda self: 100

Вы можете найти дополнительную информацию о том, как это произошло, в связанном вопросе StackOverflow , который также указывает на эту интересную внешнюю статью.

Другие вопросы по теме