how-use-python-decorators

Introduction

The decorator takes a function as an argument, adds some functionality, and returns it. Generally, decorators are used when we need to add some functionality to the existing code.

Things need to know before getting started with decorators

We know that everything in python is an object including function, method, and class. Names that we define are just identifiers bound to the objects.

def fun(text):
	return text
	
print(fun("hello world"))
f2 = fun
print(f2("hello world"))

Output

hello world
hello world

Here in both cases, we got the same output. Both fun and f2 refer to the same function object.

In python, a function can be passed to another function as an argument. Map, filter and reduce are examples of it.

Let’s see an example

def first():
	print("hello i am from function first")
	
def dec(func):
	print("hello i am from dec function")
	func()
	print(func.__name__+ " is passed inside dec")
	
dec(first)

Output

hello i am from dec function
hello i am from function first
first is passed inside dec

Here the first function is passed to the dec function. Inside the dec function, first, we print a message that says it is from the dec function and we called a function that is passed to it which is actually the first function.

In python, the function can be defined inside a function and can return it.

Let’s see an example

def Outsider():
	def inner():
		print("hello i am inner function")
	return inner
	
fun = Outsider()
print(fun())

Output

hello i am inner function

Inside the Outsider function, another function inner is defined. The Outsider function returns the whole inner function. And whenever the Outsider function is called the inner function is returned.

 

Getting started with a decorator

Let’s take an example in which a function performs a division but doesn’t do exception handling. If we want to add exception handling part we do it by decorator

def div_decorator(func):
	def inner(a,b):
		try:
			return a/b
		except Exception as e:
			return  e
		return func(a,b)
	return inner

#original function
def div(a,b):
	return a/b
	
div = div_decorator(div)
print("first case: ", div(5,2))
print("second case: ", div(4,0))

Output

first case:  2.5
second case:  division by zero

Here, the div function is decorated by div_decorator which adds the exception-handling part. Inside the decorator function, we define a new function named inner which does the exception-handling part and returns called function. The above format is not a standard way to create a decorator, here is how

def div_decorator(func):
	def inner(a,b):
		try:
			return a/b
		except Exception as e:
			return  e
		return func(a,b)
	return inner

@div_decorator
def div(a,b):
	return a/b	

print("first case: ", div(5,2))
print("second case: ", div(4,0))

Output

first case:  2.5
second case:  division by zero

The latter is the standard format to create a decorator.

Another simple decorator example

def decorator(func):
	def inner():
		result = func()
		return result.title()
	return inner

@decorator
def fun():
	return "hello world"
	
print(fun())

Output

Hello World

In this example, the decorator just changes the string into the title case.

Single decorator to multiple functions

def decorator(func):
	def inner(*args):
		ele = list(args)
		for i in ele[1:]:
			if i == 0:
				return "cannot divide by zero"
		return func(*args)
	return inner

@decorator
def division1(a,b):
	return a/b
	
@decorator	
def division2(a,b,c):
	return a/b/c
	
print("First try: ")
print(division1(3,5))
print(division2(3,3,1))
print("\n")

print("Second try: ")
print(division1(3,0))
print(division2(3,0,1))
print("\n")

print("Third try: ")
print(division1(0,5))
print(division2(0,3,1))

Output

First try:
0.6
1.0


Second try:
cannot divide by zero
cannot divide by zero


Third try:
0.0
0.0

Here, this decorator decorates multiple functions having different lengths of the argument. We use *args so that decorator can handle any length of input argument.

Two functions having a number of argument 2 and 3 is decorated. The inner function of the decorator takes the argument and checks whether the elements are zero or not rather than the first element. If the condition matches it returns the message else it returns the passed function itself.

Multiple decorators to a single function

def decorator1(func):
	def inner():
		text = func()
		return text.title()
	return inner
	
def decorator2(func):
	def inner():
		text = func()
		return text.split(" ")
	return inner

@decorator2
@decorator1										
def fun():
	return "hello world"
	
print(fun())

Output

['Hello', 'World']

Here function named fun is decorated by two decorators. The first decorator converts a string to a title case and the latter one splits by space. At first, decorator1 decorates a fun function and then decorator2 decorates the function.

 

Class decorator

Instead of function as a decorator, we can use class to decorate a function

class Decorator:
	def __init__(self, func):
		self.func = func
	
	def __call__(self, a, b):
		if b == 0:
			return "cannot divide by zero"
		else:
			return self.func(a,b)

@Decorator
def div(a,b):
	return a/b
	
print(div(10,2))
print(div(10,0))

Output

5.0
cannot divide by zero

The class can also be used to decorate a function. For this  __call__  method must be used because when the user tries to create an object that acts like a function, the class decorator must return an object that acts like a function.

Let’s take another example.

Suppose there is a function that returns the cube of a number. If the user wants the function to return the cube of as many numbers passed to it without affecting the original function, can be done using the decorator.

class Decorator:
	def __init__(self, func):
		self.func = func
	
	def __call__(self, *args, **kwargs):
		return list(map(lambda x: x**3, args))
		
@Decorator
def cube_function(n):
	return n**3
	
print(cube_function(1,2,3,4))

Output

[1, 8, 27, 64]

Here, the original cube_function returns a cube of a single number. Then it is decorated by using a class decorator to return cubes of multiple numbers.

Till now we’re dealing with user-defined decorators but there are some built-in decorators such as @property, @classmethod, and @staticmethod.

 

Property decorator

Let’s take an example

class Employee:
	def __init__(self, name, address):
		self.name = name
		self.address = address
		
	def show(self):
		return self.name + " lives in " + self.address
		
e1 = Employee("Ram", "Okhaldhunga")

# accessing attributes of class
print("Name: ", e1.name)
print("Address: ", e1.address)

#access method of class as an attribute
print("\naccessing method as an attribute: ")
print(e1.show)

Output

Name:  Ram
Address:  Okhaldhunga

accessing method as an attribute:
<bound method Employee.show of <__main__.Employee object at 0xaee9aca0>>

Here we got weird output. It is because we’re trying to access the method of class as an attribute. If the user wants to access the method as an attribute property decorates come into the picture

class Employee:
	def __init__(self, name, address):
		self.name = name
		self.address = address
		
	@property
	def show(self):
		return self.name + " lives in " + self.address
		
e1 = Employee("Ram", "Okhaldhunga")

# accessing attributes of class
print("Name: ", e1.name)
print("Address: ", e1.address)

#access method of class as an attribute
print("\naccessing method as an attribute: ")
print(e1.show)

Output

Name:  Ram
Address:  Okhaldhunga

accessing method as an attribute:
Ram lives in Okhaldhunga

Using the property decorator method of a class can be accessed as an attribute.

Classmethod decorator

class Employee:
	count_employee = 0
	
	def __init__(self,name, id):
		self.name = name
		self.id = id
		Employee.count_employee += 1
		
	def show(self):
		return self.name + "'s id is " + str(self.id)
	
	@classmethod		
	def employeeNumber(cls):
		return cls.count_employee

#create instances of class				
e1 = Employee("Shiv", 45)
e2 = Employee("Rajesh", 34)
e3 = Employee("JJ Thompson", 12)

print(e1.show())
print(e2.show())
print(e3.show())

#display no of employee
print("No of employee: ", Employee.employeeNumber())

Output

Shiv's id is 45
Rajesh's id is 34
JJ Thompson's id is 12
No of employee:  3

Classmethod decorator is used when the user wants to pass the class itself in methods rather than class instance object i.e self. Mostly, the classmethod decorator is used to create an additional constructor of a class.

 

Let’s take an example

class Employee:
	
	def __init__(self, name, id, address):
		self.name = name
		self.id = id
		self.address = address
			
	def display(self):
		print("Name: ", self.name)
		print("Id: ", self.id)
		print("Address: ", self.address)
		
	@classmethod
	def add_new(cls, value):
		name, id, address = value.split(" ")
		return cls(name, id, address)
		
print("First employee: ")
e1 = Employee("Rajesh", 4, "kapilvastu")
e1.display()
print("-----------------------------------\n")

print("Second employee: ")
e2 = Employee.add_new("ram 45 Rautahat")
e2.display()

Output

First employee:
Name:  Rajesh
Id:  4
Address:  kapilvastu
-----------------------------------

Second employee:
Name:  ram
Id:  45
Address:  Rautahat

Here add_new acts as a constructor __init__. This is how a classmethod decorator can be used to create multiple constructors.

 

Staticmethod decorator

class Employee:
	
	def __init__(self, name, id, address, age):
		self.name = name
		self.id = id
		self.address = address
		self.age = age
			
	def display(self):
		print("Name: ", self.name)
		print("Id: ", self.id)
		print("Address: ", self.address)
		print("Age: ", self.age)
		
	@staticmethod
	def is_eligible(age):
		if age < 18:
			return "Not eligible to work."
		elif age >= 18 and age < 65:
			return "Eligible to work."
		else:
			return "Not eligible to work."
		
#first employee		
e1 = Employee("Rajesh", 4, "kapilvastu", 23)
e1.display()
print("\n")
print(e1.is_eligible(23))
print("-----------------------------------")

#second employee		
e2 = Employee("Shiv", 41, "Rupandehi", 13)
e2.display()
print("\n")
print(e2.is_eligible(13))
print("-----------------------------------")

#third employee		
e3 = Employee("Thompson", 40, "kathmandu", 90)
e3.display()
print("\n")
print(e3.is_eligible(90))
print("-----------------------------------")

Output

Name:  Rajesh
Id:  4
Address:  kapilvastu
Age:  23

Eligible to work.
-----------------------------------
Name:  Shiv
Id:  41
Address:  Rupandehi
Age:  13

Not eligible to work.
-----------------------------------
Name:  Thompson
Id:  40
Address:  kathmandu
Age:  90

Not eligible to work.
-----------------------------------

Staticmethod is used to define a method static. The static method doesn’t take any reference argument whether it is called by an instance of class or class itself.

 

Conclusion

Decorators are used to decorate an existing function that adds functionality to an existing function without modifying the original function. We can use function as well as class to decorate a function.

Some built-in decorators are also available such as property decorator, classmethod decorator, and staticmethod decorator.

Property decorator allows the user to access the method of class as an attribute, classmethod decorator is used for creating more constructors, and staticmethod decorator is used to access the function inside the class that doesn’t belong to the class namespace.

Happy Learning 🙂

Leave a Reply

Your email address will not be published.