NOTE:
I've got a related question here:
How to access variables from a Class Decorator from within the method it's applied on?
I'm planning to write a fairly complicated decorator. Therefore, the decorator itself should be a class of its own. I know this is possible in Python (Python 3.8):
import functools
class MyDecoratorClass:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
# do stuff before
retval = self.func(*args, **kwargs)
# do stuff after
return retval
@MyDecoratorClass
def foo():
print("foo")
Now my problem starts when I try to apply the decorator on a method instead of just a function - especially if it's a method from another class. Let me show you what I've tried:
1. Trial one: identity loss
The decorator MyDecoratorClass
below doesn't (or shouldn't) do anything. It's just boilerplate code, ready to be put to use later on. The method foo()
from class Foobar
prints the object it is called on:
import functools
class MyDecoratorClass:
def __init__(self, method):
functools.update_wrapper(self, method)
self.method = method
def __call__(self, *args, **kwargs):
# do stuff before
retval = self.method(self, *args, **kwargs)
# do stuff after
return retval
class Foobar:
def __init__(self):
# initialize stuff
pass
@MyDecoratorClass
def foo(self):
print(f"foo() called on object {self}")
return
Now what you observe here is that the self
in the foo()
method gets swapped. It's no longer a Foobar()
instance, but a MyDecoratorClass()
instance instead:
>>> foobar = Foobar()
>>> foobar.foo()
foo() called from object <__main__.MyDecoratorClass object at 0x000002DAE0B77A60>
In other words, the method foo()
loses its original identity. That brings us to the next trial.
2. Trial two: keep identity, but crash
I attempt to preserve the original identity of the foo()
method:
import functools
class MyDecoratorClass:
def __init__(self, method):
functools.update_wrapper(self, method)
self.method = method
def __call__(self, *args, **kwargs):
# do stuff before
retval = self.method(self.method.__self__, *args, **kwargs)
# do stuff after
return retval
class Foobar:
def __init__(self):
# initialize stuff
pass
@MyDecoratorClass
def foo(self):
print(f"foo() called on object {self}")
return
Now let's test:
>>> foobar = Foobar()
>>> foobar.foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __call__
AttributeError: 'function' object has no attribute '__self__'
Yikes!
EDIT
Thank you @AlexHall and @juanpa.arrivillaga for your solutions. They both work. However, there is a subtle difference between them.
Let's first take a look at this one:
def __get__(self, obj, objtype) -> object:
temp = type(self)(self.method.__get__(obj, objtype))
print(temp)
return temp
I've introduced a temporary variable, just to print what __get__()
returns. Each time you access the method foo()
, this __get__()
function returns a new MyDecoratorClass()
instance:
>>> f = Foobar()
>>> func1 = f.foo
>>> func2 = f.foo
>>> print(func1 == func2)
>>> print(func1 is func2)
<__main__.MyDecoratorClass object at 0x000001B7E974D3A0>
<__main__.MyDecoratorClass object at 0x000001B7E96C5520>
False
False
The second approach (from @juanpa.arrivillaga) is different:
def __get__(self, obj, objtype) -> object:
temp = types.MethodType(self, obj)
print(temp)
return temp
The output:
>>> f = Foobar()
>>> func1 = f.foo
>>> func2 = f.foo
>>> print(func1 == func2)
>>> print(func1 is func2)
<bound method Foobar.foo of <__main__.Foobar object at 0x000002824BBEF4C0>>
<bound method Foobar.foo of <__main__.Foobar object at 0x000002824BBEF4C0>>
True
False
There is a subtle difference, but I'm not sure why.