The Simplest Tutorial for Python Decorators | by Christopher Tao | Towards Data Science

One of the most unique features of the Python programming language is the decorator. That is the at sign “@” you may have seen it everywhere in Python code. Of course, many other languages have decorator as well, but the concepts are different. In Python, the decorator can be defined extremely easily and utilised for any functions and classes.
It’s like a “wrapper” of the functions or classes that we used to add more behaviours of them without changing them. It is very useful when we want to
Add some behaviours to built-in or third-party functions/classes, so we don’t need to fork and modify the source code.Reuse these customised behaviours on multiple functions/classes.
Add some behaviours to built-in or third-party functions/classes, so we don’t need to fork and modify the source code.
Reuse these customised behaviours on multiple functions/classes.
There are many amazing Python libraries rely on Decorators, such as the attr and marshmallow that are introduced in the articles below.
Probably the Best Practice of Object-Oriented Python — Attrhttps://towardsdatascience.com/probably-the-best-practice-of-object-oriented-python-attr-d8c26c0e8a4
The Most Elegant Python Object-Oriented Programminghttps://towardsdatascience.com/the-most-elegant-python-object-oriented-programming-b38d75f4ae7b
In this article, I’ll try to demonstrate how to define and use a Python decorator in the simplest way. Hopefully, this will help people who don’t know or not quite familiar with it.
As I stated in the introduction, a Python decorator is like a wrapper of a function or a class. It’s still too conceptual. Let’s take a look at some code as follows:
def function_decorator(func):    def wrapped_func():        # Do something before the function is executed        func()        # Do something after the function has been executed    return wrapped_func
The above code is a definition of a decorator that decorates a function.
function_decorator is the name of the decorator.
wrapped_func is the name of the inner function, which is actually only used in this decorator definition.
func is the function that is being decorated.
In the inner function wrapped_func, we can do whatever before and after the func is called. After the decorator is defined, we simply use it as follows.
@function_decoratordef func():    pass
Then, whenever we call the function func, the behaviours we’ve defined in the decorator will also be executed.
BTW, the way of defining a decorator is actually using Python closure, which is another feature of Python. If you’re interested in this, please check out one of my previous articles for more detail.
towardsdatascience.com
I know, no one likes concepts only. Here is a simple example of Python decorator.
def function_decorator(func):    def wrapped_func():        print('=' * 30)        func()        print('=' * 30)    return wrapped_func
Then, let’s use this decorator.
@function_decoratordef test():    print('Hello World!')
Here is the output.
In this decorator, we output 30 equal signs “=” before and after the function is executed. Therefore, we can say that the decorator is used for adding top and bottom row of = for any functions printing something.
What if we want to decorate some functions that expect arguments? For example, the function below is taking a name and send a greeting.
@function_decoratordef greeting(name):    print(f'Hey {name}! Good Morning!')
Let’s try the same decorator that is defined in the basic usage section above.
Oh, no. we can’t do that, because the func we execute inside the inner function expect no arguments. Of course, Python provided an easy and universal solution to this problem. Let’s define the decorator again.
def function_decorator(func):    def wrapped_func(*args, **kwargs):        print('=' * 30)        func(*args, **kwargs)        print('=' * 30)    return wrapped_func
The only difference is that we added *args, **kwargs as the arguments to the inner function and pass them into the func. This will make sure that any arguments will be simply passed in, so there won’t be any problems.
If you don’t know what are *args and **kwargs, many tutorials can be easily found online.
Let me just give another example. We can certainly do more than output something above and below the decorated function. We can use the decorated function in whatever ways we like.
For example, we want to define a decorator that adds date-time before some logging functions.
from datetime import datetimedef timed_log(log_msg):    def time_added(*args, **kwargs):        return f'[{datetime.now()}] {log_msg(*args, **kwargs)}'    return time_added
In the above definition, the decorated function should originally output something, but now our decorator adds a date-time before it.
Suppose we have two different log functions, one is taking an integer as “line number” and indicates users that there is an error happened there, the other one is simply telling the user everything has done successfully.
@timed_logdef log_error(line_no):    return f'There is an error happend at line {line_no}'@timed_logdef log_done():    return 'Great! All processed done.'
Please be noticed that the decorator has been reused for both of the two functions.
Python decorator can not only decorating functions but also classes. Here I mean the class itself. If we want to change the behaviours of some class methods, then it is nothing different from the function decorator.
When we apply a decorator to a class, we are actually decorating its constructing method.
def class_decorator(_class):    def wrapped_class_constructor():        # Do something before the class is instantiated        instance = _class()        # Do something after the class has been instantiated        return instance    return wrapped_class_constructor
Therefore, we are actually adding behaviours when we are trying to get an instance of the class. Please be noticed that I’m using _class to refer to the class because class is a reserved keyword in Python (as in most other object-oriented languages).
Still too conceptual. Now, let me show you one of the typical usages of the class decorator, which is the “Singleton”.
If you’re not familiar with singleton, that’s fine. It is just a common programming design pattern that defining a class which can only have one instance. It is very useful when we define a class for the connection of some resources such as the database connection and the HTTP connection. we certainly don’t want to have new database connections everything we run a DB query. Therefore, we want to have only one connection and reuse it everywhere.
The basic idea is that we will define a dictionary and store the instance into it. Whenever we’re trying to instantiate the class, the dictionary is checked. If there is no instance, instantiate the class and return the instance. Otherwise, do not instantiate the class again, just return the existing instance.
def singleton(_class):    instances = {}    def get_instance(*args, **kwargs):        if _class not in instances:            print('Connecting to DB...')            instances[_class] = _class(*args, **kwargs)            print('Connected')        else:            print('Already has a connection, will reuse it.')        return instances[_class]    return get_instance
Now, let’s define a class of a DB connection. As an example, we don’t have to implement it, just use the pass keyword. Don’t forget to add the singleton decorator.
@singletonclass DBConnection:    def connect_to_db():        pass
**Shameless Ads** If you want to know more about Python, check out one of my previous articles about it.
towardsdatascience.com
Then, let’s instantiate the class twice.
It shows that the second time we’re not creating a new instance. We can further check whether the two variables actually referencing to the same instance.
In this article, I have introduced Python Decorate and its basic usages. We can use Python Decorator for any functions and classes. When it is decorating a class, it is actually applied to the constructor.
One of the most typical usages of decorating a class is the singleton design pattern. Indeed, there are other ways of implementing the singleton design pattern in Python and all of them has pros and cons. The most intuitive and easiest one I believe is using the decorator.
medium.com
If you feel my articles are helpful, please consider joining Medium Membership to support me and thousands of other writers! (Click the link above)