Demystifying Decorators

AkulMehra

Akul Mehra

Agenda

  • Intro to functions
  • What are decorators
  • Understanding how they work
  • Useful Examples
  • Decorators with parameters
  • Class Decorators

 

Intro to Functions

  • Reusable Code
  • Useful Block of Organized Code
  • Easiear to read
  • Take inputs and produce outputs
def func():
    pass

Examples

def hello():
    ''' Basic example of function. '''
    return 'Hello World'
print hello() 

# Output: Hello World
def add(a, b):
    ''' take a and b as args, add them and 
        return the result. '''
    return a + b

print add(2,5) 
# Output: 7

Everything in Python is an Object

Functions are too !

As an object, you can assign the function to a variable

def hello():
    print 'Hello'
hello() 
# prints 'Hello'

hi = hello
hi() 
# prints 'Hello'

Functions are too !

They can be passed around like other values (strings, integers, objects, etc.).

def add(a, b):
    return a + b

def apply(func, x, y):
    return func(x, y)

print apply(add, 2, 5)
# outputs: 7

Functions are too !

Function can be defined inside another function. That means a function can return another function.

def print_me(word):
    def inner():
        print word
    return inner

f = print_me('Hello World!')
f() 
# prints 'Hello World!'

    This is called a closure

Decorators are also closures.

Objects are data with methods attached, Closures are functions with data attached.

Decorators

    Instead, they take function object as an argument and return function object as return value.

Decorators

  • Decorators wrap Functions

  • Add Functionality

  • Modify behaviour

  • Diagnostics (timing etc)

Decorators


def my_decorator(original_function):
    def new_function(*args, **kwargs):
        print 'Entering', original_function.__name__
        result = original_function(*args, **kwargs)
        print 'Exiting', original_function.__name__
        return result
    return new_function

@my_decorator
def my_func():
	pass

Decorators

# Syntactic Sugar

@my_decorator
def my_func():
    pass

Under the hood

is same as writing:

def my_func():
    pass

my_func = my_decorator(my_func)

*args and **kwargs enable the function to accept any number of postional and keyword arguments.

 

*args is a list
**kwargs is a dictionary

*args & **kwargs

*args becomes args[0], args[1], args[2] ...

 

**kwargs becomes key1=kwargs[key1], key2=kwargs[key2] ...

*args & **kwargs

Timer decorator

import time

def timeit(func):
    def inner(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        time_taken = time.time() - start_time
        print 'Time taken is ', time_taken
        return result
    return inner


@timeit
def sleep(t=1):
    print 'sleeping for ', t
    time.sleep(t)

sleep()
# sleeping for  1
# Time taken is  1.00514817238

sleep(2)
# sleeping for  2
# Time taken is  2.00144505501

Examples

Memoization decorator

from timer_decorator import timeit


def memo(func):
    ''' Decreases the time taken by func from
        exponential to linear time. '''
    cache = {}

    def inner(*args):
        if args in cache:
            return cache[args]
        else:
            result = func(*args)
            cache[args] = result
        return result
    return inner


@timeit
@memo
def fibo(n):
    if n < 2:
        return n
    return fibo(n - 1) + fibo(n - 2)

print fibo(25)
# Without memoization: Time taken is  67.8232898712s
# With memoization: Time taken is  0.000825881958008s

Examples

Counter decorator

def count(func):
    def inner(*args, **kwargs):
        inner.counter += 1
        return func(*args, **kwargs)
    inner.counter = 0
    return inner

@count
def myfunc():
    pass

Examples

In [1]: from counter import *

In [2]: myfunc()

In [3]: myfunc()

In [4]: myfunc()

In [5]: myfunc.counter
Out[5]: 3

Chaining decorators

Bold and Italic decorators

def makebold(fn):
    def wrapped():
        return "<b>" + fn() + "</b>"
    return wrapped

def makeitalic(fn):
    def wrapped():
        return "<i>" + fn() + "</i>"
    return wrapped

@makebold
@makeitalic
def hello():
    return "hello world"

print hello() ## Outputs: "<b><i>hello world</i></b>"

Chaining decorators

Bold and Italic decorators

@makebold
@makeitalic
def hello():
    return "hello world"
def hello():
    return "hello world"

hello = makebold(makeitalic(hello))

The above syntactic sugar statement is same as:

 

Inspecting decorators

def shout(func):
    def inner(*args, **kwargs):
        ''' Inner func modifies the behaviour of original func '''
        print 'BEFORE SHOUTING'
        result = func(*args, **kwargs)
        print 'AFTER SHOUTING'
        return result
    return inner


@shout
def hello(name='World'):
    ''' returns hello with name argument '''
    print 'Hello', name

hello('PyCon HK')
# Output
BEFORE SHOUTING
Hello PyCon HK
AFTER SHOUTING

Inspecting decorators

In [1]: from shout import *
BEFORE SHOUTING
Hello PyCon HK
AFTER SHOUTING

In [2]: hello.__name__
Out[2]: 'inner'

In [3]: hello.__doc__
Out[3]: ' Inner func modifies the behaviour of original func '

Using decorators override the metadata of the original function.

Inspecting decorators

Using decorators override the metadata of the original function.

Using functools.wraps copies the metadata like name and docstring of the original function inside the decorator function

Inspecting decorators

from functools import wraps

def shout(func):
    @wraps(func)
    def inner(*args, **kwargs):
        ''' Inner func modifies the behaviour of original func '''
        print 'BEFORE SHOUTING'
        result = func(*args, **kwargs)
        print 'AFTER SHOUTING'
        return result
    return inner


@shout
def hello(name='World'):
    ''' returns hello with name argument '''
    print 'Hello', name

hello('PyCon HK')

Using wraps from functools library

Inspecting decorators

Using wraps from functools library

In [1]: from shout import *
BEFORE SHOUTING
Hello PyCon HK
AFTER SHOUTING

In [2]: hello.__name__
Out[2]: 'hello'

In [3]: hello.__doc__
Out[3]: ' returns hello with name argument '

Decorators with argument

def skipIf(condition, message):
    def dec(func):
        def inner(*args, **kwargs):
            if not condition:
                return func(*args, **kwargs)
            else:  # skip the func
                print message
        return inner
    return dec

@skipIf(False, 'Do not skip this')
def enter():
    print 'In the Enter function'

@skipIf(True, 'Skipped this function')
def exit():
    print 'In the Exit function'

enter()
exit()
# Output
In the Enter function
Skipped this function

Decorators with argument

@skipIf(False, 'Do not skip this')
def enter():
    print 'In the Enter function'

enter()
def enter():
    print 'In the Enter function'

enter = skipIf(False, 'Do not skip this')(enter)

is same as below statement

skipIf creates a decorator named dec and returns it, which is called with enter function as an argument

Decorators with argument

def make_generic(word):
    def dec(fn):
        def wrapped():
            return "<" + word + ">" + fn() + "</" + word + ">"
        return wrapped
    return dec


@make('b')  # To make bold
@make('i')  # To make italic
def hello():
    return "hello world"

print hello()  # Outputs: "<b><i>hello world</i></b>"

Make Generic ( Bold & Italic ) Decorators

Text

Class Decorators

def decorate(func):
    ''' decorate functions '''
    def inner(*args, **kwargs):
        print '*' * 20
        result = func(*args, **kwargs)
        print '*' * 20
        return result
    return inner


def decorate_class(cls):
    '''
    Takes class object as input and apply decorate
    func to all the callable functions.
    '''
    for k, v in vars(cls).items():
        if callable(v):
            setattr(cls, k, decorate(v))
    return cls

Class Decorators

@decorate_class
class Area:

    '''
    Functions to calculate area.
    '''
    def triangle(self, b, h):
        print 'Area of right triangle: ', 0.5 * b * h

    def square(self, s):
        print 'Area of square: ', s * s

    def rectangle(self, l, b):
        print 'Area of rectangle: ', l * b

Class Decorators

In [1]: from decorate_class import *

In [2]: ar = Area()

In [3]: ar.triangle(10, 15)
********************
Area of right triangle:  75.0
********************

In [4]: ar.square(10)
********************
Area of square:  100
********************

In [5]: ar.rectangle(10,15)
********************
Area of rectangle:  150
********************

Output

QUESTIONS ?

AkulMehra

akul08

akul08

Thank You

AkulMehra

akul08

akul08

Demystifying Decorator

By Akul Mehra

Demystifying Decorator

A brief Introduction on Decorators in Python.

  • 376
Loading comments...

More from Akul Mehra