In previous article about class-based decorators for classes I gave you example how one can create decorator.

Those decorators have similarities to inheritance between classes.

I will review pros and cons for class-inheritance and decorator(function/class based).

To The Point

Decorator example:

from timeit import default_timer as timer
class TimingDecorator(object):
    def __init__(self, klas):
        self.klas = klas
        self.org_calculation1 = self.klas.calculation1
        self.klas.calculation1 = self.calculation1

    def __call__(self, arg=None):
        return self.klas.__call__()

    def calculation1(self, *args, **kwargs):
        start = timer()
        self.org_calculation1(self.klas, *args, **kwargs)
        end = timer()
        print(end - start)

@TimingDecorator
class MyFancyClassTest():

    def calculation1(self, a, b):
        from time import sleep
        # i'm adding a sleep - as a test case if counter works.
        sleep(2)
        return (a+b*2)/(3*a)

fancy_object = MyFancyClassTest()
fancy_object.calculation1(50, 215)

Pros:

  • you can make an extension for class-methods without inheritance
  • you could mix inheritance with decorators
  • you still have the same class-object.

Cons:

  • all methods needs to be initialized and re-assigned at init
  • for each method you need to create another method in decorator (not flexible enough)
  • decorator is limited to this class methods - can not be reused at different class with different methods.

Function decorator example:

def counter_print(function_name):
    def wrapper(*args, **kwargs):
        start = timer()
        output_of_function = function_name(*args, **kwargs)
        end = timer()
        print(end - start)
        return output_of_function

    return wrapper

class MyFancyClassTest():

    @counter_print
    def calculation1(self, a, b):
        from time import sleep
        # i'm adding a sleep - as a test case if counter works.
        sleep(2)
        return (a+b*2)/(3*a)

fancy_object = MyFancyClassTest()
fancy_object.calculation1(50, 215)

Pros:

  • you still have the same class-object
  • you could can mix inheritance with decorators
  • you don't need to re-create assignment for predefined class-methods
  • your decorator is not limited to only this specific class. It is re-usable.

Cons:

  • Not advice to use this solution if you have a lot of code to make. It can get messy.

Inheritance example

class MyFancyClassTest():

    def calculation1(self, a, b):
        from time import sleep
        # i'm adding a sleep - as a test case if counter works.
        sleep(2)
        return (a+b*2)/(3*a)

class TimerMyFancyClassTest(MyFancyClassTest):

    def calculation1(self, *args, **kwargs):
        start = timer()
        output = super().calculation1(*args, **kwargs)
        end = timer()
        print(end - start)
        return output



fancy_object = TimerMyFancyClassTest()
fancy_object.calculation1(50, 215)

Pros:

  • your extended class does the same as decorator (prints timing for method)

Cons:

  • You need to create code that extends main code
  • It is not re-usable/adoptable to other class-methods
  • you endup with different object and comparison is a bit more complex

MIX (inheritance at decorators class)

from timeit import default_timer as timer

class MetaClassDecorator(object):
    """ A meta class decorator that forces creating methods from decorator-appended class."""
    def __init__(self, klas):
        self.klas = klas
        for klass_method in self.get_klas_methods():
            exec('self.org_'+klass_method+' = self.klas.'+klass_method)
            exec('self.klas.'+klass_method+' = self.'+klass_method)

    def get_klas_methods(self):
        """ based on https://stackoverflow.com/a/34452 """
        all_methods = [
            method_name for method_name in dir(self.klas)
            if callable(getattr(self.klas, method_name)) and method_name[0:2] != "__"
        ]
        return all_methods

    def __call__(self, arg=None):
        return self.klas.__call__()

class FancyTestDecor(MetaClassDecorator):
    """ This decorator forces 2 methods - fancy_method and test1 """

    def fancy_method(self, *args, **kwargs):
        start = timer()
        self.org_fancy_method(self.klas, *args, **kwargs)
        end = timer()
        print(end - start)

    def test1(self, *args, **kwargs):
        start = timer()
        self.org_test1(self.klas, *args, **kwargs)
        end = timer()
        print(end - start)


@FancyTestDecor
class MyFancyClassTest():

    def fancy_method(self):
        print("fancy print from fancy method")
    def test1(self):
        print("Ttest11")

fancy_object = MyFancyClassTest()
fancy_object.fancy_method()
fancy_object.test1()

Pros:

  • you re-use pre-existing architecture for decorator.
  • you create only class with methods (reusing meta-decorator-class)
  • you can create a more complex solutions as decorators for classes.

Cons:

  • All methods from MyFancyClassTest if any found, needs to be added and re-assigned to fields, because of exec for all methods that does not start with "__" private functions/methods - check docs for 2.7 python.
  • You create a non-reusable code just as in class-based decorator (unless you find other class that will have only those methods) (unless you find solution to replace the exec problem and re-assign methods without manually changing them at __init__)

Snippets

class MetaClassDecorator(object):
    """ A meta class decorator that forces creating methods from decorator-appended class."""
    def __init__(self, klas):
        self.klas = klas
        for klass_method in self.get_klas_methods():
            exec('self.org_'+klass_method+' = self.klas.'+klass_method)
            exec('self.klas.'+klass_method+' = self.'+klass_method)

    def get_klas_methods(self):
        """ based on https://stackoverflow.com/a/34452 """
        all_methods = [
            method_name for method_name in dir(self.klas)
            if callable(getattr(self.klas, method_name)) and method_name[0:2] != "__"
        ]
        return all_methods

    def __call__(self, arg=None):
        return self.klas.__call__()

class FancyTestDecor(MetaClassDecorator):
    """ This decorator counts timing of fancy_method method """

    def fancy_method(self, *args, **kwargs):
        start = timer()
        self.org_fancy_method(self.klas, *args, **kwargs)
        end = timer()
        print(end - start)

Acknowledgements

Auto-promotion

Related links

Thanks!

That's it :) Comment, share or don't :)

If you have any suggestions what I should blog about in the next articles - please give me a hint :)

See you in the next episode! Cheers!



Comments

comments powered by Disqus