9

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?

1
  • When I run this exact code I get a different error. What IDE are you using? Commented Jul 6, 2020 at 22:01

2 Answers 2

7
+50

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')
Sign up to request clarification or add additional context in comments.

2 Comments

Just a question out of curiosity, did you run the code as OP provided? I'm not understanding why I get a slightly different error and I want to find out... The error I got was: TypeError: timer() takes 1 positional argument but 2 were given I added self as a paramter to timer() to get the same error as OP.
timer wasn't declared as a static method, so self was passed as the argument for the func parameter, leaving no parameter for decorated function to be bound to.
1

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.