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 totype(x).__call__(x, arg1, ...)
.
Can anyone explain what is going on here?
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'