Decorators in Python

Prayukti Jain
The Startup
Published in
6 min readSep 17, 2020

--

Python is considered to be a general purpose programming language because of it’s simplified syntax which emphasis on natural language. Hence, it can be used for developing desktop or web-based application. Also, codes in python can be easily written, interpreted and executed much faster than any other language. Apart from this, it provides a huge number of predefined modules and libraries for performing complex and scientific calculations or for any Artificial Intelligence based application.

Decorators in python are those useful tools which can enable python developers to modify the behavior of functions without actually modifying the structure of the function. Decorators basically allows us to wrap a function in another function order to extend the behavior of wrapped function, without permanently modifying it.

Assigning Functions to Variables:

Functions in python supports operations like being passed as an argument, modified, assigned to a variable and returned from a function. This is a fundamental concept to understand, for creating Python decorators. So to understand this, we will create a function that will return the square of the number. We will then assign that function to a variable and use the variable to call the function.

def calc_square( number ) : # function calculates square of a number
return number**2
square = calc_square
square(5) # yields 25

Defining Function inside another Function:

We will now see how we can define a function within the other function and soon we will find out how all this is relevant with creating and understanding decorators.

def wrapper(number): # Outer function
def calc_square(number): # Inner function
return number**2

return calc_square(number)
wrapper(5) # yields 25

Functions returning other Functions:

A function can also define a function within it and return that function as well.

def wrapper():
def calc_square(number):
return number**2
return calc_square # inner function is returned
square = wrapper()
square(5)

In the above example, calc_square function is defined within the wrapper function and the wrapper function return the calc_square function. Hence, wrapper function will return a function and square can be calculated by the returned function whenever required.

Creating Closure:

Python allows a nested function to access the variable in the scope of the enclosing function. This is a crucial concept in decorators and this pattern is known as a Closure. The below example will show that inner functions can access the variable from enclosing function’s scope.

def outer_function(message):
def inner_function():
print(message) # yields "Created Closure in Python"
# message is a variable from the outer_function's scope
inner_function()outer_function("Created Closure in Python")

Passing Function as Arguments:

Python also allows to pass functions as parameters to other functions like illustrated below:

def print_report(report):
print(report)
def get_report(function):
report = "It is a sunny Day!"
function(report) # call to the print_report function
get_report(print_report) # yields "It is a sunny Day!"

Creating Decorators:

Now using these above mentioned concepts, we will create a simple decorator that will add a header message for today’s report. We will do this by defining a wrapper inside an enclosing function.

def add_header(function): def with_header():
print("Welcome!")
function()
return with_header

Our decorator function, that is, add_header function takes a function as an argument. It decorates the argument function in such a way that it prints a welcome message before the function. So, now we need to design a function that prints the weather report. Also, we learnt earlier that we can assign a function to a variable earlier, so we will use that here to call the decorator.

def print_report():
print("It is a rainy day.")
decorate = add_header(print_report)
decorate()

Now, python provides a much easier way for us to apply decorators. Simply using @ symbol prior to the function we would like to decorate, like illustrated below:

@add_header
def print_report():
print("It is a rainy day.")
print_report()# Welcome!
# It is a rainy day.

Now, what actually is happening on using the decorators is:

As soon as python interpreter encounter this @ symbol, it modifies the function on which it has been applied to. If @ symbol was not in existence, then print_report function would be simply pointing to the instance of function that only prints a single message, that is the one we defined. Now, if @ symbol is used, then print_report function would point to the instance of a modified function that the decorator function returned, that is the one with welcome message.

So now after applying the @ symbol, when we called the print_report function, which is not same anymore as we defined but have been modified is called, and we see our report decorated with a header message.

Applying Multiple Decorators to a Single Function:

Now, we will try to apply multiple decorators to our function, that is, one will add header to our weather report and another would add footer. However, the decorators will be applied in the order that we’ve called them.

# Another decorator function for footer
def add_footer(function):
def with_footer():
function()
print("Thank You!")
return with_footer

Now, applying both of them to our function:

@add_footer
@add_header
def print_report():
print("It is a rainy day.")
print_report()# Welcome!
# It is a rainy day.
# Thank You!

From the above output, we notice that the application of decorators is from the bottom up. For this the decorators must have been stored in some kind of data structures that follow, Last in First Out principle, as first the footer decorator would have been pushed and then the header one. And while decorating, the header one is decorated prior to the footer one. So, this is the proof of concept for LIFO principle.

Also, in this case the output won’t be affected if the decorators are interchanged, because of the programming done in the decorator functions. But in other cases, interchanging the order could diversely affect the output.

Decorator Function with Arguments:

Often we might need to define a decorator that accepts arguments. We can achieve this by passing the arguments to the wrapper function. The arguments will then be passed to the function that is being decorated at call time. Better illustration is given below:

def add_header(function):
def with_header(weather):
print("Welcome!")
function(weather)
return with_header
@add_header
def print_report(weather):
print("It is a {0} day.".format(weather))
print_report('Rainy')# Welcome!
# It is a Rainy day.

General Purpose Decorators:

To define a general purpose decorator that can be applied to any function, we need to use *args and **kargs. These will collect all positional and keyword arguments and stores them in the variables respectively. Also, these allows us to pass variable number of arguments during function calls.

Function with No Parameter:

def decorator(function):
def wrapper(*args,**kargs):
print("Positional arguments are", args)
print("Keyword arguments are", kargs)
function(*args)
return wrapper
@decorator
def random_function():
print("Random function with no arguments.")
random_function()# Positional arguments are ()
# Keyword arguments are {}
# Random function with no arguments.

Function with Positional arguments:

def decorator(function):
def wrapper(*args,**kargs):
print("Positional arguments are", args)
print("Keyword arguments are", kargs)
function(*args)
return wrapper
@decorator
def random_function(message):
print("Random function with {0} arguments.".format(message))
random_function('Positional')# Positional arguments are ('Positional',)
# Keyword arguments are {}
# Random function with Positional arguments.

Function with Keyword arguments:

def decorator(function):
def wrapper(*args,**kargs):
print("Positional arguments are", args)
print("Keyword arguments are", kargs)
function()
return wrapper
@decorator
def random_function():
print("This has shown keyword arguments")
random_function(argument_type='Keyword')# Positional arguments are ()
# Keyword arguments are {'argument_type': 'Keyword'}
# This has shown keyword arguments

Passing Arguments to Decorators:

Now let’s see how we can pass arguments to the decorator itself. In order to achieve this, we define a decorator maker that accepts arguments then define a decorator inside it, which would be the actual decorator and then we will define a wrapper function inside the decorator as we did earlier.

def add_header(heading):
def actual_header_adder(function):
def with_header(message):
print(heading)
function(message)
return with_header
return actual_header_adder
@add_header("Weather Report")
def print_report(message):
print("It will be a {0} day.".format(message))
print_report("Rainy")# Weather Report
# It will be a Rainy day.

Conclusion:

In this article, we learned about the concept of decorators. Decorators dynamically alter the functionality of a function or class without directly change the source code of the function or class being decorated. Using decorators in Python, one can also ensure the code doesn’t repeat unnecessarily. Do look out for other articles to get the knowledge about various topics and feel free to drop a comment for doubts or suggestions.

What are the top 10 useful functions of Underscore.js?

What are Decorators in Python?

Multithreading in Java

Understanding Apache Derby using Java

TCP/IP Socket Programming in Java

How to send HTTP Requests using React JSX?

--

--

Prayukti Jain
The Startup

Software Engineer at Microsoft | ex - Walmart | Content Writer | Open to Learn and Help