Search

11 February, 2018

Python Decorators with simple examples


Everyone who has used python , has sooner or later come across decorators . That's that thing sitting on a function which magically does something special . It looks like :


@mydecorator
def my_function


Today we are going to learn , how to write decorators of our own . How this works:



That @mydecorator is nothing but another function that's going to run along with the function it's been applied to . If we need to define a decorator function , we can say :

It is a function, that takes another function as an input argument, does some processing, and returns another function.


#Example: A decorator for function with no arguments

def square(func):
    def inner_func():
        return func() ** 2
    return inner_func

@square
def number():
    return 5


Designing Steps of the above example:

1. We define a decorator function like a normal function. Example : square function (Refer above example). 
2. It takes one argument . That argument will be a function. 
3. Inside the function , we define another function (name doesn't matter) and return that function argument itself (or with some processing like above) .
4. Finally , we return the inner function but, without parenthesis. So it doesn't execute. 

Now when  this function that we defined is applied to another, it of course gives you a square of a number. But what's happening behind the scene is


1. An object of the inner_function type gets created .
                f = square(number)

2.  f

<function inner_func at 0x00000000032892E8>

3. f ()

            25


Now what if we wanted to apply the decorator on a function that accepts an argument . Like our square function above. 




#Example with function that takes arguments (In this case,  a string)

def reverse_string(func):
    def inner_func(*args, **kwargs):
        text1, text2 = func(*args, **kwargs)
        print 'Reversed String:' + text1[::-1] + text2[::-1]
    return inner_func
@reverse_string
def get_string(s, s1):
    return s, s1

>>> get_string('Let"s' , 'decorate')
Reversed String:s"teLetaroced


Decorator with arguments

What if we want to pass arguments with the decorator itself. Let's see an example.

def multiplyby(num):
    def middlefun(func):
        def inner_fun(*args, **kwargs):
            value = func(*args)
            return value * num
        return inner_fun
    return middlefun

@multiplyby(10)
def mynum(num):
    return num

print mynum(5)
>>> 50


Let's build a cool time measuring decorator that can measure execution time of any function.



# A decorator to measure function time

def measure_this(func):

    import time
    def inside_func(*args, **kwargs):
        t1 = time.time()
        my_func = func(*args, **kwargs)
        t2 = time.time()
        print 'It took {} seconds to run the function: {}'.format(t2 - t1, func.__name__)

    return inside_func

@measure_this
def lets_wait(t):
    import time
    time.sleep(t)
    return

#Example usage:

>>> lets_wait(1)
It took 1.0 seconds to run the function: lets_wait
>>> lets_wait(3)
It took 3.0 seconds to run the function: lets_wait


We can design a decorator using classes too. 

But before we get into that , I want to tell you something about a special method called 

__call__ 

According to Python docs, Class instances are callable only when the class has a __call__() method . See the example below.


>>> class Hello:
        def __init__(self, name):
            self.name = name
        def __call__(self):
            return 'Calling %s' % self.name
 
>>> h = Hello('Arc')
>>> h
<__main__.Hello instance at 0x0000000003159508>
>>> h()
'Calling Arc'

If we don't define that __call__ method, and we try to run the class instance, we get an error


>>> h()

Traceback (most recent call last):
  File "<pyshell#26>", line 1, in <module>
    h()

AttributeError: Hello instance has no __call__ method

With this knowledge , we go ahead and design a class decorator

# Class based decorator

class decorator_class(object):
    def __init__(self, func):
        self.original_func = func

    def __call__(self, *args, **kwargs):
        return 'Hi ' + self.original_func(*args, **kwargs)

@decorator_class
def hello(name):
    return name

>>> hello('Batman')
'Hi Batman'

No comments:

Post a Comment