Decorators in Python are one of the most essential and powerful features. So, in this article, we will first discuss the prerequisites of Decorators before discussing Decorators in Python with examples of how to use them with classes and functions.
A decorator used in a function adds some functionality and returns it. That means it is a feature of Python which adds functionality to the existing code. Modifying another part of the program within the compile time is known as metaprogramming.
Because of this feature (extending its behavior without explicitly modifying it), its applications are in the real world, like debugging, logging, measuring execution time, authentication, etc.
We must be aware of how functions work and what one means by inner functions, higher-order functions, and first-class objects.
A function takes input(s), i.e., argument(s) and returns a value, i.e., output. A function may have a print which is a side effect because it gives output to the console directly.
The below function is without the print statement:
Functions are first-class Objects in Python. This means it is the same as any other variable in the language. So we can assign it to a variable of function or a variable.
Properties of first-class functions - It is an instance of Object type, returns function from function, and stores the function in a variable or data structures like list, tables, or hash.
The below function is with the print statement:
Here, name and my_name will print the same output as they refer to the same function object.
We can pass a function to another function, like passing a key function to sort lists. Here, we can also use a decorator, which we will see later.
function name accepts a function and calls it twice in its body. There is another function, my_name which is passed to the function name, so “Ramesh” is printed twice.
We can return a function from another function and can also use the returned function just like a usual variable:
These functions are known as nested functions because of their nested properties. A function is called inside a function. The inner function is called as Inner function or nested function.
one_child and another_child are the Inner functions of the parent function. These inner functions are locally scoped only; otherwise, you will get NameError when you try to call outside the scope. However, inner functions can access global functions or variables.
The "decorator" calls its inner function "wrapper" before and after decorating the "status" function. The "wrapper" function referenced to original function "status" as "fun", calling in between prints. It means a decorator can modify a function by wrapping it. Thus no need to change the original function.
The above code is not a standard way to represent decorators because we have to write the same function many times, so there is another way to use decorators in Python. The syntax is given as below:
It makes it easier to express or to read.
Now, apply this syntax in the above code -
We have only added "@decorator" above our function "status" and removed "status = decorator(status)". The output does not change.
To help with debugging and documentation, there are names and docstring in Python. We can change these because of the wrapper function in decorators.
Though it is syntactically correct but this changed the meaning we want because "status" took the place of "wrapper" or points it. That’s why it does not show the original function name and its docstring.
We need to use "functools.wraps" on the wrapper function to preserve these function name and the docstring. The updated code is
Now it is the correct name with the correct docstring from the status function. That means if we want to preserve the original name and docstring of the decorated function, we should use functools.wrap as given above.
We can reuse decorators because these are just regular functions in Python. Consider the following example
Function "thrice" is a simple decorator which calls fun 3 times. You can reuse the function "thrice" any number of times that you want just by importing it.
Hence, we learnt that decorator can be reused like functions.
Consider the following example where the function has parameters and is decorated
It will throw an error
TypeError: wrapper() takes 0 positional arguments but 1 was given
This error occurs because of the wrapper function. If it accepts any number arguments, you should fix it as given below
*args and **kwargs are useful when we need to allow any number of parameters being passed to a function.
To understand this, let's consider a function that multiplies two numbers and returns the result. We will also use a decorator from with above thrice decorator
The resultant value of multiplication is None because the wrapper function does not return anything. So we need to add a return in the wrapper function to get the desired value.
Now we have the desired value of the multiplication. Also, we have returned to the last fun only. Hence we make sure that the last fun has returned. Otherwise, it will be lost.
So decorators also have arguments. You should define a decorator inside another function with arguments that can be used inside the decorator and return it.
Let us generalize the thrice to n times, meaning we were repeating three times only, but now we will repeat at a given value.
The innermost wrapper function def wrapper(*args, **kwargs): takes a variable then calls decorated function num, that how many times have to repeat and returns the value given by the decorated function. Other inner function def decorator_times(fun): returns the wrapper function. Outermost function def times(num): takes an argument and goes to inner functions.
Hence you can also use arguments to a decorator. Only you need to wrap them inside another decorator function.
As the name says, we chain multiple decorators to a single function.
Order matters here, first to_upper and then split_string will be used. It can be written as split_string(to_upper(mess)) and assigned to mess or as given below -
Note that we can also apply multiple decorators in a function.
If you know Python classes, you can understand this topic quickly. You have learned decorators on functions. Now you will learn decorators with classes which are also known as Fancy Decorators in Python.
These are types - Decorating methods of the class and decorating complete class.
There are built-in decorators for decorating methods of a class
@classmethod - bound to class but not object, shared with the object. Class is the first parameter to pass.
@staticmethod - part of the class namespace and can not modify object state or class state.
@property - creates getter, setter, deleter.
In this example, we have included all the three types - @classmethod, @staticmethod, and @property . Class name is browser and with_incognito is a factory method that creates incognito window objects.
It is very similar to writing a function decorator. Decorator receives a class but not a function as argument which is the only difference between decorating function and decorating class. When you decorate a class, then class does not decorate its methods. See its structure which is similar as decorating function as previous
Consider the example, decorating a Class -
Hence, a decorator can be used whole or with the methods only. Note that when you use a decorator used with the whole class then it's not added with a method of the same class.
You can use decorators with classes and also you can also use classes as decorator. To store the state of the data, classes can be used. In the given example, we will implement a stateful decorator with a class which records states of data, here number of classes made for a function.
You need __init__ and __call__ to make a class as a decorator. These take function as argument and class will be used as decorator and it implements __call__ method. Decorator must be a callable object. Instead of functools.wraps, we will use functools.update_wrapper when class as a decorator.
After decoration, __call__ method is called instead of the mess method of the class, which means, by implementing __call__ method and passing the function to __init__ as argument, the class works as a decorator.
There are various real world uses of decorators, for logging and debugging code, Validation JSON, Caching return values of a function and authorization framework like Django.
We have an example code that measures the execution time of a function.
So, the decorators are first class objects in Python which extend the behavior of code without changing its function, with the help of decorator, i.e,. @decorator. We can use a decorator in function or method of a class or in the whole class, and also classes can also be used as decorator. Decorator has various applications like logging, authentication, and measuring execution time.