Search code examples
pythonpython-3.xsetattr

setattr for `__call__` method of an object doesn't work in Python


I though that for python object obj() is equivalent to obj.__call__(). But it looks like it doesn't hold when setattr was used.

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

Why is it so? And how I can set the call method in runtime?

Notice in Python version is 3.9.18 and 3.11.4

The snippet of code above, I'd expected a() returns 100


Solution

  • The issue is because __call__ is defined at the class level, not at the instance level. See the documentation (from Python 2, when this was introduced) here.

    You'd need to make sure to define an instance-level call function and apply setattr to that:

    class MyClass:
        def __call__(self, *args, **kwargs): 
            return self._call(self, *args, **kwargs)
    
    >>> a = MyClass()
    >>> setattr(a, '_call', lambda self: 100)
    >>> a()
    100
    

    Or, you can patch the class itself, if you want every instance of MyClass() to have the exact same __call__ function. However, consider carefully if you want to do this:

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

    Explanation: Prioritising the class definition of __call__ over any instance-specific definition is common to the various special method names/operator overloads in Python's data model.

    It comes from when new-style classes were introduced in Python 2. To quote the "order of precedence" introduced in (PEP 252):

    There’s a conflict between names stored in the instance dict and names stored in the type dict. If both dicts have an entry with the same key, which one should we return? [...]

    1. Look in the type dict. If you find a data descriptor, use its get() method to produce the result. This takes care of special attributes like dict and class.
    2. Look in the instance dict. If you find anything, that’s it. (This takes care of the requirement that normally the instance dict overrides the class dict.)

    The "type dict" refers to class-level definitions, such as the MyClass' __call__ definition, while the "instance dict" refers to values we have set on a specific instance of MyClass, such as a above.

    In your question, the initial __call__ definition is coming from step 1 (the type dict), while any values you set like so are set in the instance dict, which has lower priority:

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

    You can find more information about how this came to be in this related StackOverflow question which also points to this interesting external article.