Decorators in Python: Extending functionality of a function

Category: PythonTags: function python intermediate

Decorators are one of the most powerful concepts in python. A decorator is a function which takes another function or wraps a function to improve the behaviour of that function.

generators in python

A decorator is a function which takes another function and extends its functionality without actually modifying it and finally returns it.
In this tutorial, we will learn how to write decorators and why we should use it?

Types of decorators in Python

  1. Function Decorators
  2. Class Decorators

A decorator in Python is any callable Python object that is used to modify a function or a class.

Prerequisites to understand decorators

  1. In python everything (even classes) are objects.
    So, functions are also an object and we can assign different reference variables or names to it.
  2. def greet(name):
      print('hello ' + name)
      
    greet('abhishek')
    
    wish = greet
    wish('abhishek')

    Output

    hello abhishek
    hello abhishek
  3. In python, functions can also be passed as an argument.
  4. def greet(func, name):
      func(name)
    
    def sayHello(name):
      print('Hello ' + name)
    
    def sayBye(name):
      print('Bye ' + name)
    
    greet(sayHello, 'abhishek')
    greet(sayBye, 'abhishek')

    Such functions are known as higher-order functions.

    Output

    Hello abhishek
    Bye abhishek
  5. A function can also return another function.
  6. def greet(name):
      def sayHello():
          print('Hello ' + name)
      return sayHello
    
    wish = greet('abhishek')
    wish()

    Output

    Hello abhishek

    These are basics, which are required in order to understand decorators in python.

Decorators in Python

Basically, decorators are also a function but they accept functions as arguments and may perform some modifications to it.

def make_pretty(func):
    def inner():
        print('*************************')
        func()
        print('*************************')
    return inner
  
def sayHello():
    print('Hello world')
  
decorated_func = make_pretty(sayHello)
decorated_func()

Output

*************************
Hello world
*************************

Here, you can observe that we are taking a function as an argument and defining a completely new function by calling that function and finally returning our new function which can be used now as a new version of our function.

Syntactic sugar to use decorators

We can use @ symbol with the name of decorator function to decorate our functions and it works in the same way.

Syntax
@decorator_func_name
def func_name():
  ....

func_name() # calling

Using the syntactic sugar for decorators

def make_pretty(func):
  def inner():
      print('*************************')
      func()
      print('*************************')
  return inner

@make_pretty
def sayHello():
  print('Hello world')

sayHello()

Output

*************************
Hello world
*************************

Decorator function with parameters

Example 1:
def repeat_twice(func):
  def inner(name):
      func(name)
      func(name)
  return inner
   
@repeat_twice
def greet(name):
    print('Welcome ' + name)
  
greet('Abhishek')

Output

Welcome Abhishek
Welcome Abhishek
Example 2:
def smart_greet(func):
  def inner(name):
      if name == 'Abhi':
          print('Welcome ' + name)
      else:
          print('Welcome guest!')
  return inner

@smart_greet
def greet(name):
  print('Welcome ' + name)

greet('Abhi')
greet('Abhishek')

Output

Welcome Abhi
Welcome guest!
Example 3:
def smart_divide(func):
  def inner(a, b):
      if b == 0:
          print('Unable to divide!')
      else:
          func(a, b)
  return inner

@smart_divide
def divide(a, b):
  print(a // b)

divide(10, 0)
divide(10, 5)

Output

Unable to divide!
2

Returning values from a decorated function

We can also return some values from a function which is being decorated.

def returning_func(func):
  def inner(name):
      return func(name)
  return inner

@returning_func
def greet(name):
  return 'Welcome ' + name

print(greet('Abhishek'))

Output

Welcome Abhishek

Chaining decorators in python

We can even chain multiple decorators to a function. They will be executed sequentially in order.

Example 1:
def star(func):
  def inner(name):
      print('*' * 20)
      func(name)
      print('*' * 20)
  return inner

def percent(func):
  def inner(name):
      print('%' * 20)
      func(name)
      print('%' * 20)
  return inner

@star
@percent
def greet(name):
  print('Welcomme ' + name)
greet('Abhi')

Output

********************
%%%%%%%%%%%%%%%%%%%%
Welcome Abhi
%%%%%%%%%%%%%%%%%%%%
********************
Example 2:
def star(func):
  def inner(name):
      print('*' * 20)
      func(name)
      print('*' * 20)
  return inner

def percent(func):
  def inner(name):
      print('%' * 20)
      func(name)
      print('%' * 20)
  return inner

@percent
@star
def greet(name):
  print('Welcome ' + name)

greet('Abhi')

Output

%%%%%%%%%%%%%%%%%%%%
********************
Welcome Abhi
********************
%%%%%%%%%%%%%%%%%%%%

Using class as a Decorator

We will rewrite following function decorator as class decorator.

def greet(func):
  def inner():
      print('*' * 20)
      func()
      print('*' * 20)
  return inner

@greet
def greet_abhi():
  print('Welcome Abhi')

greet_abhi()

Output

********************
Welcome Abhi
********************

Following decorator works similar as above but uses class instead of function.

class greet:
  def __init__(self, func):
      self.func = func

  def __call__(self):
      print('*' * 20)
      self.func()
      print('*' * 20)

@greet
def greet_abhi():
  print('Welcome Abhi')

greet_abhi()

Output

********************
Welcome Abhi
********************