Michael Carilli
Michael Carilli

Reputation: 411

Lookup rules for callable class: A() vs A.__call__()

I've been reading through this excellent post in an effort to better understand metaclasses, and encountered some behavior that confuses me.

Setup

class AMeta(type):
    # Disclaimer:  this is not what a real-world metaclass __call__ should look like!
    def __call__(self, *args):
        print("AMeta.__call__, self = {}, args = {}".format(self, args))

class A(metaclass=AMeta):
    def __call__(self, *args):
        print("A.__call__, self = {}, args = {}".format(self, args))

Question 1

A.__call__(1, 2) prints A.__call__, self = 1, args = (2,).

Args are forwarded directly (1 appears in the spot of self) so it appears no descriptor machinery is triggered. Based on these lookup rules this could be because AMeta.__call__ is a non-data descriptor (and therefore does not take precedence over A.__call__) or it could be because __call__ is a magic method that is therefore given some special direct lookup by the interpreter.

I'm not sure which of these two hypothesis is correct, if either. If anyone knows the answer, I'd appreciate it!

Question 2 A(1, 2) prints AMeta.__call__, self = <class '__main__.A'>, args = (1, 2).

The metaclass's __call__ gets invoked, which agrees with the portrayal here of AMeta's __call__ being the puppet master during creation of A instances.

Why does the metaclass's __call__ get invoked here? What lookup rule is at play when I invoke A(1, 2) constructor-style? Clearly A(1, 2) is not just syntactic sugar for A.__call__(1, 2).

I've seen a number of other questions that dance around these questions but none that appear to answer them directly.

I'm using/concerned with Python 3.7+.

Upvotes: 1

Views: 108

Answers (1)

pppery
pppery

Reputation: 3814

Question 1:

Although you are indeed correct that __call__ is a magic method that is handled specially by the interpreter, that special behavior is only triggered when you say A() and implicitly delegate to the __call__ method. Saying A.__call__ executes in exactly the same way as a.foo, meaning that it comes down to, as you said, AMeta.__call__ being a non-data descriptor and therefore being overwritten by the explicit definition of A.__call__

Question 2:

Unlike in Question 1, the interpreter's special handling of magic methods does get invoked here. However, including metaclasses complicates things a little. The test case is roughly equivalent to the below code without metaclasses:

class A:
    def __init__(self):
        self.__call__ = self.call2
    def call2(self, *args):
        print("call2, self = {}, args = {}".format(self, args))
    def __call__(self, *args):
        print("__call__, self = {}, args = {}".format(self, args))

That special handling of magic methods I talked about above is that when trying to implicitly call magic methods using syntax like A(1, 2), Python ignores any attributes set directly on the object, and only cares about attributes set on the type.

Thus, A(1, 2) is sugar for roughly type(A).__call__(A, 1, 2) (except for the fact that any __getattr__ and __getattribute__ methods on the type are ignored). This explains why the __call__ method on the metaclass gets called.

Upvotes: 1

Related Questions