Scaler Ads

Decorators in Python – How to enhance functions without changing the code?

Python

Decorators in python allow you to dynamically change the functionality of another function, without altering it’s code.

What? Is that possible?

Yes.

This covers:
1. What is a decorator and how to create one?
2. Easier way to decorate functions
3. Class decorators
4. Problem with docstrings on decorated functions and how to solve.

What is a decorator in Python?

Decorator is a function that takes another function as an argument, adds some additional functionality, thereby enhancing it and then returns an enhanced function.

All of this happens without altering the source code of the original function.

Let’s see it in action.

Let’s suppose, you have a function that computes the hypotenuse of a triangle.

# Compute Hypotenuse
def hypotenuse(a, b):
    return round(float((a*a) + (b*b))**0.5, 2)

hypotenuse(1,2)

Output:

#> 2.24

Example use case:

Let’s just say, you happen to have many such functions defined in your python code, getting executed in a elaborate way.

To keep a track, you want to print out what function is getting executed before actually running it, so you can monitor the flow of logic in your python code.

Here, at the same time, you don’t want to change the actual content of 'Hypotenuse' or any of the other functions, because obviously since it’s harder to manage larger functions.

So what do we do?

Create a decorator of course.

Get Free Complete Python Course

Facing the same situation like everyone else?

Build your data science career with a globally recognised, industry-approved qualification. Get the mindset, the confidence and the skills that make Data Scientist so valuable.

Logo

Get Free Complete Python Course

Build your data science career with a globally recognised, industry-approved qualification. Get the mindset, the confidence and the skills that make Data Scientist so valuable.


# Decorator that takes and print the name of a func.
def decorator_showname(myfunc):
    def wrapper_func(*args, **kwargs):
        print("I am going to execute: ", myfunc.__name__)
        return myfunc(*args, **kwargs)
    return wrapper_func

Note, wrapper_func receives (*args and **kwargs)

# Decorate Hypotenuse
decorated_hyp = decorator_showname(hypotenuse)
decorated_hyp(1,2)
#> I am going to execute: hypotenuse
#> 2.24

Nice. It displayed the custom message showing the name of the function before executing hypotenuse().

Notice, the content of hypotenuse itself has not changed. Very nice!

The great news is: it can decorate any function and not just 'hypotenuse‘.

So, if you want to do the same for, say a func to calculate circumference, you can simply decorate it like this and it will work just fine.

# Dummy example
decorated_circ = decorator_showname(circumference)

Nice.

Easier way to decorate functions

But, is there an easier way? Yes.

Simply add @decorator_showname before the function you want to decorate.

# Method 1: Decorate WITH the @ syntax
@decorator_showname
def hypotenuse2(a, b):
    return round(float((a*a) + (b*b))**0.5, 2)

hypotenuse2(1,2)
#> I am going to execute: hypotenuse2
#> 2.24

Basically what you are doing here is, decorate hypotenuse2 and reassign the decorated function to the same name (hypotenuse2).

# Method 2: Decorate WITHOUT the @ syntax.
def hypotenuse2(a, b):
    return round(float((a*a) + (b*b))**0.5, 2)

hypotenuse2 = decorator_showname(hypotenuse2)
hypotenuse2(1,2)
#> I am going to execute: hypotenuse2
#> 2.24

Both approaches are really the same. In fact, adding the @decorator_func wrapper does what method 2 did.

How to create Class Decorators?

While decorator functions are common in practice. Decorators can also be created as classes, bringing in more structure to it.

Let’s create one for the same logic but using class.

class decorator_showname_class(object):
    def __init__(self, myfunc):
        self.myfunc = myfunc

def __call__(self, *args, **kwargs):
    print("I am going to execute: ", self.myfunc.__name__)
    return self.myfunc(*args, **kwargs)

To make this work, you need to make sure:

  1. The __init__ method takes the original function to be decorated as the input. This allows the class to take an input.
  2. You define the wrapper on the dunder __call__() method, so that the class becomes callable in order to function as a decorator.
@decorator_showname_class
def hypotenuse3(a, b):
    return round(float((a*a) + (b*b))**0.5, 2)

hypotenuse3(1,2)

Output:

#> I am going to execute: hypotenuse3
#> 2.24

Problem with Decorators: The docstring help is gone?!

When you decorate a function, the docstring of the original decorated function becomes inaccessible.

why?

Because the decorator takes in and returns an enhanced but a different function. Remember?

# Before decoration
def hypotenuse2(a, b):
    """Compute the hypotenuse"""
    return round(float((a*a) + (b*b))**0.5, 2)

help(hypotenuse2)

Help on function hypotenuse2 in module main:

hypotenuse2(a, b)
Compute the hypotenuse

Now, let’s decorate and try again.

# Docstring becomes inaccesible
@decorator_showname
def hypotenuse2(a, b):
    """Compute the hypotenuse"""
    return round(float((a*a) + (b*b))**0.5, 2)

help(hypotenuse2)
#> Help on function wrapper_func in module main:

#> wrapper_func(*args, **kwargs)

The help does not show the docstring  :(.

So how to deal with this?

The Solution

It’s because of this reason, everytime when someone writes a decorator, they always wrap the wrapping function with another decorator called @functools.wraps(func) from the functools package.

It simply updates the wrapper function with the docstring of the original function.

It’s quite easy to use:

  1. Just make sure functools.wraps decorates the wrapper function that the decorator returns.
  2. It receives the function whose documentation to adopt as the argument.
import functools

# Add functools docstring updation functionality
def decorator_showname(myfunc):
    @functools.wraps(myfunc)
    def wrapper_func(*args, **kwargs):
        print("I am going to execute: ", myfunc.__name__)
        return myfunc(*args, **kwargs)
    return wrapper_func

Try decorating now, the docstring should show.

# decorating will show docstring now.
@decorator_showname
def hypotenuse2(a, b):
    """Compute the hypotenuse"""
    return round(float((a*a) + (b*b))**0.5, 2)

help(hypotenuse2)

Practice Problems:

Create a decorator to log start time, end time and the total time taken by the function to run.

Course Preview

Machine Learning A-Z™: Hands-On Python & R In Data Science

Free Sample Videos:

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science