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?
An abstract class can be considered as a blueprint for other classes. It allows you to create a set of methods that must be created within any child classes built from the abstract class. A class which contains one or more abstract methods is called an abstract class.
ABCMeta metaclass provides a method called register method that can be invoked by its instance. By using this register method, any abstract base class can become an ancestor of any arbitrary concrete class.
To declare an abstract method, use this general form: abstract type method-name(parameter-list); As you can see, no method body is present. Any concrete class(i.e. class without abstract keyword) that extends an abstract class must override all the abstract methods of the class.
In object-oriented programming, an abstract class is a class that cannot be instantiated. However, you can create classes that inherit from an abstract class. Typically, you use an abstract class to create a blueprint for other classes. Similarly, an abstract method is an method without an implementation.
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')
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With