Reputation: 159
I have an abstract base class in Python which defines an abstract method. I want to decorate it with a timer function such that every class extending and implementing this base class is timed and doesn't need to be manually annotated. Here's what I have
import functools
import time
import abc
class Test(metaclass=abc.ABCMeta):
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'apply') and
callable(subclass.apply))
@abc.abstractmethod
def apply(self, a: str) -> str:
raise NotImplementedError
def timer(func):
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer
def __getattribute__(self, name):
if name == "apply":
func = getattr(type(self), "apply")
return self.timer(func)
return object.__getattribute__(self, name)
class T2(Test):
def apply(self, a: str) -> str:
return a
if __name__ == '__main__':
t = T2()
t.apply('a')
The error I get is as follow
Traceback (most recent call last):
File "/Users/blah/test.py", line 41, in <module>
t.apply('a')
File "/Users/blah/test.py", line 20, in wrapper_timer
value = func(*args, **kwargs)
TypeError: apply() missing 1 required positional argument: 'a'
I think understand the error python thinks that the apply method of the T2() object is a classmethod
however I am not sure why given that I call getattr(type(self), "apply")
. Is there a way to get the instance method?
Upvotes: 9
Views: 217
Reputation: 3105
I played a bit with your code and this seems to be working:
import abc
import functools
import time
class TimingApply(abc.ABCMeta):
def __new__(cls, *args):
inst = super().__new__(cls, *args)
inst.apply = cls.timer(inst.apply)
return inst
def timer(func):
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer
class Test(metaclass=TimingApply):
@abc.abstractmethod
def apply(self, a: str) -> str:
raise NotImplementedError
class T2(Test):
def apply(self, a: str) -> str:
return a
class T3(T2):
def apply(self, a: str) -> str:
time.sleep(0.1)
return a + a
if __name__ == "__main__":
t = T2()
t.apply("a")
t3 = T3()
t3.apply("a")
I used slightly different approach - moved all the boilerplate code to metaclass and left base class (Test
) clean. A __new__
special method is used to instantiate the class instance, so I do a brutal replacement of method by wrapped one.
Upvotes: 1
Reputation: 531878
Use __init_subclass__
to apply the timer decorator for you. (timer
, by the way, doesn't need to be defined in the class; it's more general than that.) __init_subclass__
is also a more appropriate place to determine if apply
is callable.
import abc
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer
class Test(metaclass=abc.ABCMeta):
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# ABCMeta doesn't let us get this far if cls.apply isn't defined
if not callable(cls.apply):
raise TypeError("apply not callable")
cls.apply = timer(cls.apply)
@abc.abstractmethod
def apply(self, a: str) -> str:
raise NotImplementedError
class T2(Test):
def apply(self, a: str) -> str:
return a
if __name__ == '__main__':
t = T2()
t.apply('a')
Upvotes: 7