Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 958
Krish Avatar asked Jun 18 '20 19:06

Krish


People also ask

Can a class have 2 abstract methods in Python?

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.

What is ABCMeta in Python?

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.

What decorator we use to declare abstarct methods?

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.

What is the use of abstract method in Python?

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.


2 Answers

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')
like image 69
chepner Avatar answered Oct 08 '22 16:10

chepner


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.

like image 41
Tupteq Avatar answered Oct 08 '22 16:10

Tupteq