Decorators in python allow you to dynamically change the functionality of another function, without altering it’s code.
What? Is that possible?
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)
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.
Want to become awesome in ML?
Hi! I am Selva, and I am excited you are reading this!
You can now go from a complete beginner to a Data Science expert, with my end-to-end free Data Science training.
No shifting between multiple books and courses. Hop on to the most effective way to becoming the expert. (Includes downloadable notebooks, portfolio projects and exercises)
Start free with the first course 'Foundations of Machine Learning' - a well rounded orientation of what the field of ML is all about.
Sold already? Start with the Complete ML Mastery Path
# 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
wrapper_func receives (
# 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
Notice, the content of
hypotenuse itself has not changed. Very nice!
The great news is: it can decorate any function and not just
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)
Easier way to decorate functions
But, is there an easier way? Yes.
@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 (
# 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:
__init__method takes the original function to be decorated as the input. This allows the class to take an input.
- 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)
#> 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.
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:
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?
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
It simply updates the wrapper function with the docstring of the original function.
It’s quite easy to use:
- Just make sure
functools.wrapsdecorates the wrapper function that the decorator returns.
- 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)
Create a decorator to log start time, end time and the total time taken by the function to run.