pythonpython-descriptorspython-datamodel

Python dunder methods wrapped as property


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?


Solution

  • 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'