Open-Source Internship opportunity by OpenGenus for programmers. Apply now.
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 parameterskwargs
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.