The Power of Python Decorators

Chandra Shekhar Sahoo
5 min readDec 17, 2020

In this blog we will learn how python decorators works, how to create and use them.

A decorator is a software design pattern which allows to alter the functionality of a function , method, or class dynamically without having to directly use subclasses or change the source code of the function being decorated.

But before visiting python Decorators we first know some basic concept about python functions:

  1. Function as “First Class Objects”
def print_first(name):
print(f"{name} is my favorite language")


def print_second(name):
print(f"{name} is easy to learn")


def print_func(func_name):
return func_name("Python")

print_func(print_first)
print_func(print_second)

Output:

Python is my favorite language
Python is easy to learn

Here we are passing function as arguments. “python_first()” and “python_second()” are regular functions that expect a name given as a string. The “print_func()” function however, expects a function as arguments.

2. Inner Functions

def favorite_lang():
print("Favorite language function")

def print_python():
print("Python is my favorite language")

def print_java():
print("Java is my favorite language")

print_python()
print_java()

favorite_lang()

Output:

Favorite language function
Python is my favorite language
Java is my favorite language

Note : The inner functions are not defined until the outer function is called. Inner functions are locally scoped to "favorite_lang()” as local variabless. Calling inner functions which throw and error as function not defined.

3. Returning Functions from functions

def favorite_lang(key):

def print_python():
return "Python is my favorite language"

def print_java():
return "Java is my favorite language"

if key == 1:
return print_python
else:
return print_java

lang1 = favorite_lang(1)
lang2 = favorite_lang(2)

print(lang1)
print(lang1())

print(lang2)
print(lang2())

Output:

<function favorite_lang.<locals>.print_python at 0x000001C9D1BD38C8>
Python is my favorite language
<function favorite_lang.<locals>.print_java at 0x000001C9D1D74AE8>
Java is my favorite language

“lang1” and “lang2” are reference to the functions “print_python()” and “print_java()” respectively. Calling “lang1()” and “lang2()” will call functions “print_python()” and “print_java()” respectively.

Now we are good to explore the beauty of Python Decorators.

Simple or Basic Decorators

Let’s understand with example by defining decorator function

def decorator_function(func):
def wrapper_function():
print("Function Execution starts here")
func()
print("Function Execution ends here")
return wrapper_function

Now we can use this decorator in two ways:

a. Using simple calls

def execute_func():
print("Executing Function ......")

execute_func = decorator_function(execute_func)

execute_func()

Output:

Function Execution starts here
Executing Function ……
Function Execution ends here

b. Using “Syntactical Sugar”

@decorator_function
def execute_func():
print("Executing Function ......")

execute_func()

Output:

Function Execution starts here
Executing Function ……
Function Execution ends here

Note: we have used the “@decorator_function” which we have defined above.

This means

“@decorator_function” is similar to

“execute_func = decorator_function(execute_func)”.

Also note as per definition decorators wrap a function, modifying its behavior. Calling “execute_func” function now prints two extra print statements as wrapper function.

The wrapper function more generic way:

def decorator_function(func):
@functools.wraps(func)
def wrapper_function(*args, **kwargs):
print("Function Execution starts here")
func(*args, **kwargs)
print("Function Execution ends here")
return func(*args, **kwargs)
return wrapper_function

“decorator_function” = Decorator methods that encapsulates the wrapper function

“wrapper_function” = Wrapper Function which contains the wrapping logics

“execute_func” = Decorated Function is the original function

Few things to observe here:

a. The wrapper function returns the return of the decorated function.

b. Also we can pass the arguments of the decorated function to the wrapper function.

c. @functools.wraps(func) Used to preserves the information about the original function like “__name__” and other properties.

Advance Decorators

Now we are bit comfortable with simple decorators. Lets us know understand the some Advance decorators.

  1. Decorator with classes

a. Decorator used on class methods

from decorators import debug, timer

class Place:
@debug
def __init__(self, max_rate):
self.max_rate= max_rate
self.rating = None
@timer
def rating(self, rating):
sleep(1000)
self.rating = rating
place = Place(5)
Calling __init__(<place.Place object at 0x7efccce059608>, 5)
'__init__' returned None


place.rating(3)
Finished 'rating' in 0.43876 secs

Here we used ‘@debug’ and ‘@timer’ decorators defined in the decorators package.

b. Decorate the whole class

from decorators import debug, timer
@timer
class Place:
def __init__(self, max_rate):
self.max_rate= max_rate
self.rating = None
def rating(self, rating):
sleep(1000)
self.rating = rating2.
place = Place(5)

Output:

Finished ‘Place’ in 0.0000 secs

Here decorating a class does not decorate its methods. @timer only measures the time it takes to instantiate the class.

2. Nested Decorators

We can multiple apply decorators in a single functions.

from decorators import debug, timer

@debug
@timer
def display(name):
print("Hello World!")

Note: Output of the above implementations is not equal to

from decorators import debug, timer

@timer
@debug
def display(name):
print("Hello World!")

3. Decorators with Arguments

@repeat(num_times=10)
def display(name):
print("Hello World!")

Here we pass the argument to decorator function repeat.

4. Classes as Decorators

We can implement class as decorator using “__init__()” and “__call__()” method inside the class. The class name will be used as decorator and it need to take function name as argument in its constructor . i.e “__init__()” method. We use “__call__”() method to make the class instance callable. “.__call__()” method is executed each time when we call the instance of the class.

import functools

class FuncCountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count_calls = 0

def __call__(self, *args, **kwargs):
self.count_calls += 1
print(f"Calling {self.func.__name__}")
print(f"Call count : {self.count_calls}")
return self.func(*args, **kwargs)

@FuncCountCalls
def check_count():
print("Hello World!")

check_count()
check_count()

Output:

Calling check_count
Call count : 1
Hello World!
Calling check_count
Call count : 2
Hello World!

Note: We have to use the functools.update_wrapper() function instead of @functools.wraps.

“__call__()” works similar to wrapper function used in simple decorators.

5. Stateful Decorators

Sometimes as per requirement we need to keep the track of state. It can remeber the state of their previous run.

from functools import update_wrapper


class Tally:
def __init__(self, func):
update_wrapper(self, func)
self.func = func
self.tally = {}
self.n_calls = 0

def __call__(self, *args, **kwargs):
self.n_calls += 1
self.tally[self.func.__name__] = self.n_calls

print("Callable Tally:", self.tally)
return self.func(*args, **kwargs)


@Tally
def hello(name):
return f"Hello {name}!"


print(hello("Redowan"))
print(hello("Nafi"))

Output:

Callable Tally: {‘hello’: 1}
Hello Redowan!
Callable Tally: {‘hello’: 2}
Hello Nafi!

Here’s a stateful decorator called Tally that will keep track of the number of times decorated functions are called in a dictionary. The keys of the dictionary will hold the names of the functions and the corresponding values will hold the call count.

Conclusion

In this blog we explored the beauty of Python decorators by understanding the simple decorators to advance decorators. In larger projects especially, decorators give us much more flexibility to encapsulate various operations without exposing the irrelevant APIs to other modules.

Will try to bring some more exciting topics in future blogs. Till then:

Code Everyday and Learn Everyday ! ! !

References

  1. https://book.pythontips.com/en/latest/decorators.html
  2. https://www.python.org/dev/peps/pep-0318/
  3. https://dbader.org/blog/python-decorators

--

--

Chandra Shekhar Sahoo

Freelancers, Python Developer by Profession and ML Enthusiasts