Demystifying Decorators

AkulMehra

Akul Mehra

🇮🇳

Agenda

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

 

Intro to Functions

  • Reusable Code (DRY principle)
  • 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.

It extends the behavior of the latter function without explicitly modifying it

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.005326747894287

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

Examples

Memoization decorator

import time

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


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

start_time = time.time()
print(fibo(40))
print('Time taken: ', time.time() - start_time)
# Without memoization: Time taken is  54.21900486946106s
# With memoization: Time taken is  0.00011396408081054688s

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
def polynomial_creator(a, b, c):
    def polynomial(x):
        return a * x**2 + b * x + c
    return polynomial
    
p1 = polynomial_creator(2, 3, -1)
x = 2

print('x: ', x, 'p(x): ', p1(x))

# Output:
# x: 2, p(x): 13
# 2 * 2**2 + 3 * 2 + (-1) = 13
p(x) = a*x^2 + b*x + c
p(x)=ax2+bx+cp(x) = a*x^2 + b*x + c

Examples

POLYNOMIAL FUNCTIONS

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('HDE Team')
# Output
BEFORE SHOUTING
Hello HDE Team
AFTER SHOUTING

Inspecting decorators

In [1]: from shout import *
BEFORE SHOUTING
Hello HDE Team
AFTER SHOUTING

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

In [3]: hello.__doc__
Out[3]: ' Inner func modifies the behavior 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('HDE Team')

Using wraps from functools library

Inspecting decorators

Using wraps from functools library

In [1]: from shout import *
BEFORE SHOUTING
Hello HDE Team
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(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

Flask uses decorators extensively

class App:
    route_functions = {}
    def route(url_pattern):
        def wrap(f):
            route_functions[url_pattern] = f
            return f
        return wrap

@app.route decorator

@app.route(“/”)
def index():
    return “Hello world”
index = app.route(“/”)(index) = wrap(index) = index

# index is added to the route_functions mapping with url pattern.
route_functions['/'] = index

Flask uses decorators extensively

def login_required(f):
    def wrap(*args, **kwargs):
        # if user is not logged in, redirect to login page      
        if not request.headers["authorization"]:
            return redirect("login page")
        # get user via some ORM system
        user = User.get(request.headers["authorization"])
        # make user available down the pipeline via flask.g
        g.user = user
        # finally call f. f() now haves access to g.user
        return f(*args, **kwargs)
    return wrap

@login_required decorator

@app.route(“/”)
@login_required
def index():
    return “Hello world”

QUESTIONS ?

AkulMehra

akul08

akul08

Thank You

AkulMehra

akul08

akul08

Demystifying Decorators

By Akul Mehra

Demystifying Decorators

A brief Introduction on Decorators in Python.

  • 818