Krish
Krish

Reputation: 159

Decorators on Python AbstractMethods

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

Answers (2)

Tupteq
Tupteq

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

chepner
chepner

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

Related Questions