Python dunder methods wrapped as property

104 Views Asked by At

I stumbled upon this code that I found weird as it seems to violate the fact that python builtins call dunder methods directly from the class of the object. Using __call__ as an example, if we define class A as following:

class A:
    @property
    def __call__(self):
        def inner():
            return 'Called.'
        return inner

a = A()
a() # return 'Called.'

type(a).__call__(a) # return 'property' object is not callable. 

However, this behaviour seems to contradict with what's said in Python's official documentation:

object.__call__(self[, args...]) Called when the instance is “called” as a function; if this method is defined, x(arg1, arg2, ...) roughly translates to type(x).__call__(x, arg1, ...).

Can anyone explain what is going on here?

2

There are 2 best solutions below

0
jsbueno On BEST ANSWER

Yes, but it respects the way to retrieve a method from a given function - We can see that the __get__ method is called:

On the code bellow, I just replaced property with a simpler descriptor that will retrieve its "func" - and used it as the __call__ method.


In [34]: class X:
    ...:     def __init__(self, func):
    ...:         self.func = func
    ...:     def __get__(self, instance, owner):
    ...:         print("descriptor getter called")
    ...:         return self.func
    ...: 

In [35]: class Y:
    ...:     __call__ = X(lambda: "Z")
    ...: 

In [36]: y = Y()

In [37]: y()
descriptor getter called
Out[37]: 'Z'

So, the "dunder" functionality just retrieved the method through its __get__ as usual for all methods. What was skipped is the step that goes through __getattribute__ - Python will go directly to the __call__ slot in the A class, and not go through the normal lookup sequence, by calling __getattribute__, which starts at the class (for a descriptor), then looks at the instance, than back to the class (for a regular attribute): it assumes that if there is something at the instance's class dunder slot it is a method, and uses its __get__ method accordingly.

A function's __get__ is used when retrieving it from an instance - as that is the mechanism that injects the instance as the self argument for the call.

And __get__ is exactly the thing that property replaces to perform its "magic".

To demonstrate that __getatribute__ is not called, in iPython, I had to encapsulate "y" inside a dummy function, otherwise iPython would trigger __getattribute__ when trying to autocomplete stuff:


In [42]: class Y:
    ...:     __call__ = X(lambda: "Z")
    ...:     def __getattribute__(self, name):
    ...:         print("getattribute called")
    ...:         return super().__getattribute__(name)
    ...: 

In [43]: def w():
    ...:     y = Y()
    ...:     return y()
    ...: 

In [44]: w()
descriptor getter called
Out[44]: 'Z'
# in contrast with:

In [46]: Y().__call__()
getattribute called
descriptor getter called
Out[46]: 'Z'


0
user2357112 On

When the Python language internals look up magic methods to implement language features, they bypass __getattribute__ and the instance dict. However, they do not bypass the descriptor protocol.

a() finds the __call__ property you defined, finds that it's a descriptor, and calls descriptor.__get__(a, A). This delegates to the getter you wrote, which returns an inner function. Then Python treats this inner function as the result of the __call__ lookup, and calls it.

type(a).__call__(a) is a manual call to __call__, so it goes through the regular attribute lookup process. It finds the __call__ property you defined, and finds that it's a descriptor, but since you're looking up the property on the class instead of the instance, it calls descriptor.__get__(None, A). This returns the property object itself, instead of calling the property getter. Then you attempt to call the property as a function, and get a TypeError.

(The docs you're quoting say x(arg1, arg2, ...) roughly translates to type(x).__call__(x, arg1, ...). It's not a completely exact equivalence, but if they put something in those docs that handles every weird special case, it would have taken way too much code and obscured the point.)