A decorator is a design pattern in Python that allows a user to add new functionality to an existing function or object without modifying its structure. Decorators are usually called before the definition of a function you want to decorate.
Functions as First-Class Objects
Before diving into decorators we need to understand how functions work in Python where they are first-class objects. That means that functions can be passed around and used as arguments, just like any other object (string, int, etc.)
def sleep(name): return name + " sleeping" def eat(name): return name + " eating" def human_action(action_function): return action_function("John") human_action(sleep) # returns: John sleeping
Functions that take other functions as arguments are also called higher-order functions.
In addition, a function can return another function:
def eat(): def prepare_food(): print("Preparing food") return prepare_food call_function = eat() call_function() # prints: Preparing food
Here, prepare_food() is an inner function which is defined and returned, each time we call eat()
Decorators
Think that we have a normal function such as the one below.
def normal_function(): print("Do something")
And we want to add some more functionality to it. This can be achieved with a decorator function. Using decorator we can provide a function, add some functionality to it and return it
def my_decorator(func): def inner_decorator(): print("Adding decorator") func() return inner_decorator def normal_function(): print("Do something") decorated_function = my_decorator(normal_function) decorated_function() # This will output: # Adding decorator # Do something
The function normal_function() got decorated and the returned function was given the name decorated_function.
The decorator function (my_decorator()) added new functionality to the original function. The decorator acts as a wrapper. The nature of the object that got decorated (nomral_function() in this example) does not alter. But now, it behaves differently since it got decorated.
We decorate a function and reassign it as (Line 10 above):
decorated_function = my_decorator(normal_function)
This is very common and Python provides some syntactic sugar to simplify this, by using @decorator_function above the definition of the function we want to decorate.
@my_decorator def normal_function(): print("Do something") normal_function()
This is exactly the same as the assignment we did it earlier:
decorated_function = my_decorator(normal_function)
Chaining Decorators
We can have as many decorators as we need for any function.
def my_first_decorator(func): def inner_decorator(): print("Adding first decorator") func() print("End of first decorator") return inner_decorator def my_second_decorator(func): def inner_decorator(): print("Adding second decorator") func() print("End of second decorator") return inner_decorator @my_first_decorator @my_second_decorator def normal_function(): print("Do something") normal_function() # Output: # Adding first decorator # Adding second decorator # Do something # End of second decorator # End of first decorator
Always remember that the order in which we chain decorators does matter.
Function Parameters in Decorators
The above examples are very simple and do not take into consideration any parameters that we may want to pass to the decorated functions.
We can pass the decorated function’s parameters in the inner function of our decorator and do something with them at the inner function.
Python provides the magic of (*args, **kwargs) where args is the tuple of positional arguments and kwargs the dictionary of the keyword arguments. With this in mind, we can create decorators that can work with any function no matter the parameters it needs.
def smart_decorator(func): def inner(*args, **kwargs): for arg in args: print(arg) for kwarg in kwargs.items(): print(kwarg) print("I can decorate any function") return func(*args, **kwargs) return inner @smart_decorator def people_attirbutes(name,surname,age): print(name,surname,age) people_attirbutes("John", "Doe", 32) people_attirbutes(age=28, name="Mary", surname="Jane")
The output of the above is:
John Doe 32 I can decorate any function John Doe 32 ('age', 28) ('name', 'Mary') ('surname', 'Jane') I can decorate any function Mary Jane 28
In the first call, we print all the args that are provided before actually executing the decorated function and in the second call we do the same but for the keyword arguments.
Please note that the way this is implemented it will print both arguments and keywords if both exist.
This was a basic intro to the Python Decorators and their usage. In this particular topic, there are several more advanced concepts to be covered such as:
- Decorators With Arguments
- Usage of @functools.wraps
- Usage of classes as Decorators