Python Decorators simplified

Shashi Kumar Raja
TestCult
Published in
7 min readMay 24, 2020
Decorators in action

In this article we are going to learn about decorators in python.

Use case without Decorator-

We will start with writing a simple function which returns a string. Assume that all these code are written in the same file for simplicity and make sure to write the decorator function before the actual function where decorator is used.

# A function returning a string
def get_name():
return "World"

This function will give following output-

Now, we want to greet this person whose name we got through the get_name function call. To achieve this we will write a greet function like below-

# A function appending Hello before a name
def greet(name):
return "Hello {}".format(name)

Let’s pass function get_name as argument in the function greet and save the result in a variable to get the desired output-

greeting = greet(get_name())
value of variable greeting

So far, what we did is simply pass a function(which is a callable as it can be called) as an argument to another function, which took the actual output of first function and changed/enhanced it to give a different/desired output.

This is what decorator does.

A Python decorator is any callable Python object (i.e an object which has a __call__ method like a function or class) which modifies a function or a class.

Same use case with Decorator-

Now, let’s try to achieve the same result using decorators this time. We will re-write our greet function to make it a decorator.

# It takes function as an input
def greet(function):
def wrapper():
#store the output of function in a variable
name = function()
greeting = "Hello {}".format(name)
return greeting
return wrapper

Here,

  • greet takes a function as an argument.
  • Inside greet there is another function named wrapper (you can name it anything you want) which calls the argument function and stores its result in name variable.
  • It also appends “Hello” before the output of argument function call and returns that value.
  • In the end we return wrapper.

We need to modify our get_name function to use this decorator using “@” symbol which is used to denote a decorator. Ensure that the greet function is written before the get_name function.

@greet
def get_name():
return "World"

This is how the code and output should look like-

Decorator in action

Note that using @greet before get_name definition is just a syntactic sugar. It is equivalent to calling the greet and get_name function like below which will give same output-

get_name = greet(get_name)print(get_name())

This is a simple decorator in action.

Function with positional arguments-

Let’s proceed with a bit more complex scenario.

Assume that our get_name function accepts an argument name and if there is a valid name value it returns that else it returns “John Doe” -

@greet
def get_name(name):
if name:
return name
return "John Doe"

Now, we need to modify our greet decorator in such a way that along with the function it also accepts the argument name being passed to the function-

def greet(function):
def wrapper(*args):
name = function(*args)
greeting = "Hello {}".format(name)
return greeting
return wrapper

Here,

  • wrapper function accepts an *args list.You can have as many positional arguments in the function and the decorator will handle it here.
  • it calls the argument function with the same args list to get its value

To test it, we will call the get_name function twice -

  • With valid name “World” argument where it will print the passed name with greetings.
  • With an empty string as name argument where it should print John Doe with greeting.

This is how the entire code should look like-

Decorator with *args

Function with both positional and keyword arguments-

Similar to the above scenario, our get_name function can have keyword arguments as well. Assume that get_name function has a keyword argument called salutation which should be appended before the name for the greeting-

@greet
def get_name(name, salutation=None):
if name:
if salutation:
return "{} {}".format(salutation, name)
else:
return name
return "John Doe"

Now, to support this use case we will modify our greet decorator to handle keyword arguments as well-

def greet(function):
def wrapper(*args, **kwargs):
name = function(*args, **kwargs)
greeting = "Hello {}".format(name)
return greeting
return wrapper

Here,

  • wrapper function accepts both *args and **kwargs. You can have as many positional and keyword arguments in the function and the decorator will handle it here.
  • it calls the argument function with the same args and kwargs list to get its value.

Lets test this with follow test cases-

  • with valid name and salutation- this should print greeting with name and salutation both
  • with valid name but invalid salutation- this should print greeting with only name without any salutation
  • with invalid name- this should print greeting for John Doe

This is how the code and output should look like-

Function with args and kwargs

So far, we learnt how to handle functions with positional and keyword arguments. How about a use case where decorator itself has a positional argument and modifies the passed function based on its value.

Decorator with positional arguments-

Assume our get_name function is still same, but we want to greet a person nicely or rudely based on some condition.

# Decorator with positional argument value rudely
@greet('rudely')

def get_name(name, salutation=None):
if name:
if salutation:
return "{} {}".format(salutation, name)
else:
return name
return "John Doe"

To implement this, we need to add support of positional argument in our decorator function greet-

def greet(*decorator_args):
def outer_wrapper(function):
def wrapper(*args, **kwargs):
name = function(*args, **kwargs)
if 'rudely' in decorator_args:
greeting = "Get lost {}".format(name)
elif 'nicely' in decorator_args:
greeting = "Welcome {}".format(name)
else:
greeting = "Hello {}".format(name)
return greeting
return wrapper
return outer_wrapper

Here,

  • decorator greet accepts *decorator_args which is equivalent to *args. You can have as many positional arguments in the Decorator call.
  • function outer_wrapper now accepts the passed function and wrapper like before is handling the args and kwargs of the passed function.
  • Inside wrapper, based on the value of decorator_args we are changing the greeting message.

Let’s test the function for three use cases-

  • with @greet(‘rudely’): this should print- Get lost Mr World
  • with @greet(‘nicely’): this should print- Welcome Mr World
  • with empty argument value to greet- this should print Hello Mr World

The code and output should look like-

Rude greeting with decorator

Similarly we can implement decorator accepting keyword arguments as well.

Issues caused by Decorator usage-

Since decorator works by wrapping functions, the wrapper hides the original function’s metadata-

  • name,
  • its positional and keyword argument lists,
  • its docstrings if any

This can be a big hinderance while debugging the issues caused by the code to actually pin point the issue location.

For example- in our get_name function, if we try to access its __name__, we will see output wrapper instead of get_name which is function’s original name.

Solution to the issue-

Another python decorator comes as a saviour to solve this issue :)

Python’s functools module provides a decorator named @functools.wraps(). If you use this decorator to wrap your function, then the original function’s metadata are retained.

It is the recommend way of writing decorators in python. Let’s update our decorator function greet to use this decorator. I’ll use the first decorator we implemented for the sake of brevity-

import functoolsdef greet(function):
@functools.wraps(function)
def wrapper():
name = function()
greeting = "Hello {}".format(name)
return greeting
return wrapper

Now, let’s access __name__ of get_name to validate the implementation.

Original metadata is retained now

Summary-

To summarize, Python Decorator-

  • enables us to modify the existing function behavior without modifying the source code of actual function
  • helps in implementing Don’t Repeat Yourself (DRY) while coding
  • makes the code more readable.

We learnt the basic concept of decorators and used them in python functions. Similarly decorators can be used with Classes as well as class methods.

Happy coding :)

--

--