Decorators and class method
-
02-07-2021 - |
Question
I am having trouble understanding why the following happens. I am having a decorator which does nothing except it checks whether a function is a method. I thought I have understood what method in Python is, but obviously, this is not the case:
import inspect
def deco(f):
def g(*args):
print inspect.ismethod(f)
return f(*args)
return g
class Adder:
@deco
def __call__(self, a):
return a + 1
class Adder2:
def __call__(self, a):
return a + 2
Adder2.__call__ = deco(Adder2.__call__)
Now, running the following:
>>> a = Adder()
>>> a(1)
False
2
>>> a2 = Adder2()
>>> a2(1)
True
3
I would expect for this code to print True two times.
So, decorating the function manually as in the Adder2 is not an exact equivalent to decorating via the @deco function?
Can someone be so glad and explain why this happens?
Solution
Inside a class definition, __call__
is a function, not a method. The act of accessing the function through attribute lookup (e.g. using the dot syntax), such as with Adder2.__call__
, returns an unbound method. And a2.__call__
returns a bound method (with self
bound to a2
).
Note that in Python3 the concept of unbound method
has been dropped. There, Adder2.__call__
is a function as well.
OTHER TIPS
Methods are functions that are associated with a class. Methods are only created when you retrieve them from an already defined class; a method is a wrapper around a function, with a reference to the class as well (and optionally a reference to the instance).
What happens in the first case is: Python compiles your class definition Adder
. It finds the decorator definition and a function. The decorator is passed the function, returning a new function. That function is added to the class definition (stored in the class __dict__
). All this time you are dealing with a python function, not a method. That happens later.
When you then call a(1)
, a lookup reveals that the instance doesn't have a __call__
but the Adder
class does, so it is retrieved using __getattribute__()
. This finds a function (your deco
decorator), which is a descriptor so it's __get__()
method is called (so Adder.__call__.__get__(a, Adder))
, returning a bound method, which is then called and passed in the 1
value. The method is bound because instance
is not None when __get__()
is called. Your decorator, which wrapped a function at class building time, prints False
because it was passed an unwrapped function to start with.
In the second case, however, you retrieve a method (again via __getattribute__()
calling __get__()
on the undecorated Adder2.__call__
function), this time unbound (as there is no instance, only a class passed to __get__()
(the full call is Adder2.__call__.__get__(None, Adder2)
), and you then decorate that method. Now ismethod()
prints True.
Note that in Python 3, the latter case changes. In Python 3 there no longer is a concept of an unbound method, only functions and bound methods. The term 'bound' is thus dropped altogether. Your second case would also print False
as Adder2.__call__.__get__(None, Adder2)
returns a function in that case.