Python Programming/Decorators


Duplicated code is recognized as bad practice in software for lots of reasons, not least of which is that it requires more work to maintain. If you have the same algorithm operating twice on different pieces of data you can put the algorithm in a function and pass in the data to avoid having to duplicate the code. However, sometimes you find cases where the code itself changes, but two or more places still have significant chunks of duplicated boilerplate code. A typical example might be logging:

def multiply(a, b):
    result = a * b
    log("multiply has been called")
    return result

def add(a, b):
    result = a + b
    log("add has been called")
    return result

In a case like this, it's not obvious how to factor out the duplication. We can follow our earlier pattern of moving the common code to a function, but calling the function with different data is not enough to produce the different behavior we want (add or multiply). Instead, we have to pass a function to the common function. This involves a function that operates on a function, known as a higher-order function.

Decorator in Python is a syntax sugar for high-level function.

Minimal example of property decorator:

>>> class Foo(object):
...     @property
...     def bar(self):
...         return 'baz'
...
>>> F = Foo()
>>> print(F.bar)
baz

The above example is really just a syntax sugar for codes like this:

>>> class Foo(object):
...     def bar(self):
...         return 'baz'
...     bar = property(bar)
...
>>> F = Foo()
>>> print(F.bar)
baz

Minimal Example of generic decorator:

>>> def decorator(f):
...     def called(*args, **kargs):
...         print('A function is called somewhere')
...         return f(*args, **kargs)
...     return called
...
>>> class Foo(object):
...     @decorator
...     def bar(self):
...         return 'baz'
...
>>> F = Foo()
>>> print(F.bar())
A function is called somewhere
baz

A good use for the decorators is to allow you to refactor your code so that common features can be moved into decorators. Consider for example, that you would like to trace all calls to some functions and print out the values of all the parameters of the functions for each invocation. Now you can implement this in a decorator as follows:

#define the Trace class that will be 
#invoked using decorators
class Trace(object):
    def __init__(self, f):
        self.f =f

    def __call__(self, *args, **kwargs):
        print("entering function " + self.f.__name__)
        i=0
        for arg in args:
            print("arg {0}: {1}".format(i, arg))
            i =i+1
            
        return self.f(*args, **kwargs)

Then you can use the decorator on any function that you defined by:

@Trace
def sum(a, b):
    print "inside sum"
    return a + b

On running this code you would see output like

>>> sum(3,2)
entering function sum
arg 0: 3
arg 1: 2
inside sum

Alternately, instead of creating the decorator as a class, you could have used a function as well.

def Trace(f):
    def my_f(*args, **kwargs):
        print("entering " +  f.__name__)
        result= f(*args, **kwargs)
        print("exiting " +  f.__name__)
        return result
    my_f.__name = f.__name__
    my_f.__doc__ = f.__doc__
    return my_f

#An example of the trace decorator
@Trace
def sum(a, b):
    print("inside sum")
    return a + b

#if you run this you should see
>>> sum(3,2)
entering sum
inside sum
exiting sum
5

Remember it is good practice to return the function or a sensible decorated replacement for the function so that decorators can be chained.