Learn Decorators in Python with examples


Reading time: 30 minutes | Coding time: 10 minutes

Decorators are syntactic sugars, that allows modifying the behavior of function or class. We wrap another function to extend the functionality of the wrapped function, without altering its internal behaviour.

We take a look at three examples:

  • Finding time taken to execute a function using a decorator
  • Printing debug related information of a function call
  • implement Singleton pattern using decorators

Implementing a decorator using classes

To create a decorator, we create a class with def __call__(self, *argv, **kwargs): function defined. Like this -

class MyDecorator:
  def __init__(self, function):
    self.function = function

  def __call__(self, *argv, **kwargs): # Special function that will be called
    # Do something before calling original function
    self.function(*argv, **kwargs)
    # Do something after calling original function

# Using decorator
@MyDecorator
def function():
  pass

The decorator syntax uses the @ character followed by decorator name (class name) and optional parameters, that will be passed to constructor.

@MyDecorator
def function(...):
  ...

Which is roughly equivalent to

def function(...):
  ...
function = MyDecorator(function)

Implementing a decorator using functions

from functools import wraps

def MyDecorator(f):
  @wraps(f) # Preserves info. about original function, useful in debugging.
  def wrapped(*args, **kwargs):
    # Before calling
    result = f(*args, **kwargs)
    # After calling
    return result
  # Return the wrapped
  return wrapped

@MyDecorator
def function():
  pass

This is just another way of defining decorators.

  • *argv, **kwargs are used to pass around the original parameters during calls
  • *args gives all function parameters
  • kwargs gives all keyword arguments except for those corresponding to a formal parameter as a dictionary.

Applications / Examples

Some of the applications of decorators in Python are:

  • Finding time taken to execute a function using a decorator
  • Printing debug related information of a function call
  • implement Singleton pattern using decorators

Finding time taken to execute a function using a decorator

To find the execution time using decorators, we need to define a class named TimeIt over-writing the __call__ function to measure the time taken to execute the function. Note the same technique can be used to do things before and after functions.

The TimeIt class looks as follows:

class TimeIt:   
  def __init__(self, f): 
    self.f = f 

  def __call__(self, *args, **kwargs): 
    start = time() 
    result = self.f(*args, **kwargs) 
    end = time() 
    print("{} took {} seconds".format(self.f, end-start)) 
    return result 

Complete code:

from time import time, sleep

class TimeIt:   
  def __init__(self, f): 
    self.f = f 

  def __call__(self, *args, **kwargs): 
    start = time() 
    result = self.f(*args, **kwargs) 
    end = time() 
    print("{} took {} seconds".format(self.f, end-start)) 
    return result 

@TimeIt
def function(delay): 
  sleep(delay) 

function(3) 

Output:

<function function at 0x00000000033CBD68> took 3.00099992752 seconds

The above is a perfect use case for a decorator, i.e. to find execution time of a function.

Printing debug related information of a function call

functools provide decorators and higher level functions for various use case. One is functools.wraps which is a function decorator that acts as a wrapper.

It is same as:

partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)

Complete example:

from functools import wraps

def debug(f):
  @wraps(f)
  def wrapped(*args, **kwargs):
    args_repr = [repr(a) for a in args]
    kwargs_repr = [str(k) + "=" + str(v) for k, v in kwargs.items()]
    signature = ", ".join(args_repr + kwargs_repr)
    print("Calling " + f.__name__ + "(" + signature + ")")
    result = f(*args, **kwargs)
    print(f.__name__ + " returned " + str(result))
    return result 
  return wrapped

@debug
def factorial(n): 
  if n == 1:
    return 1
  return n * factorial(n-1) 

factorial(5) 

Output:

Calling factorial(5)
Calling factorial(4)
Calling factorial(3)
Calling factorial(2)
Calling factorial(1)
factorial returned 1
factorial returned 2
factorial returned 6
factorial returned 24
factorial returned 120

This another example shows how to get information about function calls. Especially helps in debugging recursive functions.

How to implement Singleton pattern using decorators?

A singleton is a class with only one instance. Singletons are like global variables.

The idea is to create a wrapper which checks if instance exists using wrapped.instance (wrapped is same as kwargs) and do accordingly.

from functools import wraps

def singleton(c):
  @wraps(c)
  def wrapped(*args, **kwargs):
    if not wrapped.instance:
        wrapped.instance = c(*args, **kwargs)
    return wrapped.instance
  wrapped.instance = None
  return wrapped

@singleton
class OneInstanceOnly:
  def __init__(self, value):
    self.value = value
  
  def printValue(self):
    print(self.value)

o1 = OneInstanceOnly(1)
o2 = OneInstanceOnly(2)

o1.printValue()
o2.printValue()

Output:

1
1

Here, the second instance is not created, instead, the 1st is returned on subsequent calls.