4

I need to change the behavior of the __call__ method of a given object. The naive approach would be something like:

class A(object):
    def __call__(self):
        return 1

def new_call():
    return 42


a = A()
a.__call__ = new_call

Why is it not the output of a() 42? Is there a workaround I can exploit to achieve the same effect? (without using the class)


============================ EDIT =================================

For the records, the short answer is no. Python calls the "special methods" like __call_ directly on the class and not on the instance, therefore if you need to change the method, you need to change it on the class itself.

Andrea
  • 2,932
  • 11
  • 23
  • First don’t you need to include the self argument in new_call? And why not just subclass the A class – Jab Feb 04 '20 at 17:02

3 Answers3

5

Special methods (aka "dunder" methods) are looked-up with respect to the class of the object, so to override it you would need to change the class, not the instance. Also note that methods all have an initial argument passed to them, usually called self.

The following would do what you want (notice how that it affects all instances of the class):

class A(object):
    def __call__(self):
        return 1

def new_call(self):
    return 42


a1 = A()
a2 = A()
A.__call__ = new_call

print(a1())  # -> 42
print(a2())  # -> 42

If you only want to change a specific instance, a relatively simple workaround is to make the class' __call_() method call one that's not "special" like it is — i.e. by introducing a level of indirection.

Here's what I mean:

# Workaround

class B(object):
    def __call__(self):
        return self.call_method(self)

    @staticmethod
    def call_method(self):
        return 1

def new_call(self):
    return 42

# Create two instances for testing.
b1 = B()
b2 = B()
b2.call_method = new_call  # Will only affect this instance.

print(b1())  # -> 1
print(b2())  # -> 42
martineau
  • 119,623
  • 25
  • 170
  • 301
4

Typically, special method names, such as __call__, are accessed via the object's type, rather than the instance's attribute collection. From Special Method Names:

If a class defines a method named __getitem__(), and x is an instance of this class, then x[i] is roughly equivalent to type(x).__getitem__(x, i).

This also applies to __call__. a() is roughly equivalent to type(a).__call__(a). Changing a's __call__ attribute doesn't have an effect, because the code is looking for A's __call__ attribute.

Assigning a new function to A.__call__ should do what you want.

class A(object):
    def __call__(self):
        return 1

def new_call(self):
    return 42


a = A()
A.__call__ = new_call
print(a())

Result:

42
Kevin
  • 74,910
  • 12
  • 133
  • 166
  • 3
    This changes `__call__` for *all* instances of `A`, though, not just `a`, which I think is not what the OP wants. – chepner Feb 04 '20 at 17:06
  • Exactly, this way I cannot change two different objects is a different way. Nevertheless thanks for the valuable explanation – Andrea Feb 04 '20 at 17:08
  • Hmm, tricky. AFAICT, special method lookup doesn't bother to look at the instance's attributes, even if the class doesn't define the method. So it's hard to get per-instance behavior. Jordan Brière's approach, or something like it, is probably the simplest way to explicitly "dispatch" the behavior of `__call__` to each instance. – Kevin Feb 04 '20 at 17:18
0

Because A.__call__ is resolved before a.__call__. If you want to bind __call__ per instance, then you have to change the resolution by attempting to resolve it from there. E.g.

class A(object):
    def __call__(self):
        try:
            return vars(self)['__call__']()
        except KeyError:
            return 1

def new_call():
    return 42


a = A()

print(a())
a.__call__ = new_call
print(a())

Would prints:

1
42

If you want it to be a method, and have access to self, then you have to bind it. E.g.

from types import MethodType

class A(object):
    def __call__(self):
        try:
            return vars(self)['__call__']()
        except KeyError:
            return 1

def new_call_method(self):
    return self

a = A()
a.__call__ = MethodType(new_call_method, a)
also_a = a()

Would return a.

Jordan Brière
  • 1,045
  • 6
  • 8
  • Don't use a bare `except`. The only exception you should be catching is the `KeyError` when the instance variable `__call__` isn't present. That implies that you should call `vars` before the `try` statement, and only attempt to call the result of `whatever['__call__']` after the `try` statement. – chepner Feb 04 '20 at 17:07
  • I'm lost here, `a()` still returns `1`, isn't it? – Andrea Feb 04 '20 at 17:12
  • No, `a()` returns `42`, because `a.__dict__` is looked first into `A.__call__` and fallback to returning `1`. – Jordan Brière Feb 04 '20 at 17:13
  • 2
    The second attempt fails because magic methods go directly to the class attribute, not an instance attribute. – chepner Feb 04 '20 at 17:14
  • 1
    The second attempt relies on the first `A` definition. I didn't re-define it, to avoid duplicating the code. – Jordan Brière Feb 04 '20 at 17:15
  • 2
    No, the point is that `a(...)` is *always* `A.__call__(a, ...)`, whether or not `a.__call__` is defined. – chepner Feb 04 '20 at 17:16
  • I tend to agree with @chepner, can you post a full code snippet, maybe I'm missing something – Andrea Feb 04 '20 at 17:17
  • 1
    It is, hence why `A.__call__` first tries to `return vars(self)['__call__']()`, which will resolve from the instance `a.__call__`, or fallback to `return 1`. – Jordan Brière Feb 04 '20 at 17:18
  • Edited the second snippet, by adding the class definition from the first one. The second was really just an extra, showing how to bind the instance so that it passes `self` to the new call method. – Jordan Brière Feb 04 '20 at 17:20
  • 1
    @JordanBrière You have two completely different solutions here: the first one works, because you generalized `A.__call__`. The second one does not, because you can't "preempt" `A.__call__` by defining `a.__call__`. That simply isn't how the `()` call syntax works. – chepner Feb 04 '20 at 17:20
  • 1
    Ok, now I got what you meant. Still, I wish I was not forced to change the class definition – Andrea Feb 04 '20 at 17:21