This article will help you understand the concept of decorators in Python programming and how best to use them. We’ll cover what Python decorators are, what their syntax looks like, how to identify them in a script or framework, and how to apply them yourself.
A function decorator in Python is just a function that takes in another function as an argument, extending the decorated function’s functionality without changing its structure. A decorator wraps another function, amplifies its behavior, and returns it.
The concept of decorators in Python help keeps your code DRY. A function decorator avoids unnecessary repetition across your codebase, because some repeated bits of code can be pulled together to become function decorators. As you advance in using Python for development, decorators can help with analytics and logging. They’re also vital for setting up validation and runtime checks.
As we proceed, I’ll assume you have a basic understanding of Python functions and programming and you have at least Python 3.8 installed on your device.
Things to Understand before Delving into Python Decorators
Table of Contents
In Python, functions are first-class objects, meaning they can receive arguments or be passed as arguments. To fully grasps the concept of decorators, there are a few things you need to understand.
A function is an object, which means it can be assigned to another variable
def greet(): print("Hello John") greet_john = greet
greet_john()
>>>
Hello John
Always remember that everything is an object in Python. In the same way you assign values to a variable, a function can also be assigned to a variable where necessary. This is important as you learn about decorators.
A function can be returned from another function
def greet(): def greeting_at_dawn(): print("Good morning") return greeting_at_dawn salute = greet()
salute()
>>>
Good morning
An inner function in Python can be returned from the outer function. This is part of the functional programming concepts you’ll come across.
A function can be passed as an argument of another function
def greet_some(func): print("Good morning", end=' ') func() def say_name(): print("John") greet(say_name)
>>>
Good morning John
A function that receives a function argument is known as a higher order function.
The above-listed points are important to keep in mind when learning to implement decorators and use them effectively in a Python program.
How Python Decorators Work
A simple decorator function starts with a function definition, the decorator function, and then a nested function within the outer wrapper function.
Always keep these two main points in mind when defining decorators:
- To implement decorators, define an outer function that takes a function argument.
- Nest a wrapper function within the outer decorator function, which also wraps the decorated function.
This is what the most basic decorator function looks like in the code snippet below:
def increase_number(func): def increase_by_one(): print("incrementing number by 1 ...") number_plus_one = func() + 1 return number_plus_one return increase_by_one def get_number(): return 5 get_new_number = increase_number(get_number)
print(get_new_number())
>>>
incrementing number by 1 ...
6
Looking at the code above, the outer function increase_number
— also known as the decorator — receives a function argument func
. increase_by_one
is the wrapper function where the decorated get_number
function is found. The decorator is assigned to another variable. This is what a decorator syntax looks like when using Python decorators. However, there’s a much easier way to represent decorators.
A simple decorator function is easily identified when it begins with the @
prefix, coupled with the decorated function underneath. The previous example can be refactored to look like this:
def increase_number(func): def increase_by_one(): print("incrementing number by 1 ...") number_plus_one = func() + 1 return number_plus_one return increase_by_one @increase_number def get_number(): return 5 print(get_number())
>>>
incrementing number by 1 ...
6
The examples show that a decorator extends the functionality of its function argument.
Decorator Functions with Parameters
There are cases where you may need to pass parameters to a decorator. The way around this is to pass parameters to the wrapper function, which are then passed down to the decorated function. See the following example:
def multiply_numbers(func): def multiply_two_numbers(num1, num2): print("we're multiplying two number {} and {}".format(num1, num2)) return func(num1, num2) return multiply_two_numbers @multiply_numbers
def multiply_two_given_numbers(num1, num2): return f'{num1} * {num2} = {num1 * num2}' print(multiply_two_given_numbers(3, 4)) >>>
we're multiplying two number 3 and 4
3 * 4 = 12
Having parameters passed to the inner function or nested function makes it even more powerful and robust, as it gives more flexibility for manipulating the decorated function. Any number of arguments (*args
) or keyword arguments (**kwargs
) can be passed unto the decorated function. *args
allows the collection of all positional arguments, while the **kwargs
is for all keyword arguments needed during the function call. Let’s look at another simple example:
def decorator_func(decorated_func): def wrapper_func(*args, **kwargs): print(f'there are {len(args)} positional arguments and {len(kwargs)} keyword arguments') return decorated_func(*args, **kwargs) return wrapper_func @decorator_func
def names_and_age(age1, age2, name1='Ben', name2='Harry'): return f'{name1} is {age1} yrs old and {name2} is {age2} yrs old' print(names_and_age(12, 15, name1="Lily", name2="Ola"))
>>>
There are 2 positional arguments and 2 keyword arguments
Lily is 12 yrs old and Ola is 15 yrs old
From the above example, *args
forms an iterable of positional arguments as a tuple, while the **kwargs
forms a dictionary of keyword arguments.
Multiple Decorators or Decorator Chaining in Python
There are several options to explore when using function decorators in your Python project. Another use case would be chaining decorators (two or more) to a function. A function can be decorated with more than one decorator (multiple decorators), and this is achieved by stacking one decorator on top of the other in no particular order. You’ll have the same output no matter the order in which the multiple decorators are placed on top of each other, as seen in the following example:
def increase_decorator(func): def increase_by_two(): print('Increasing number by 2') new_number = func() return new_number + 2 return increase_by_two def decrease_decorator(func): def decrease_by_one(): print('Decreasing number by 1') new_number = func() return new_number - 1 return decrease_by_one @increase_decorator
@decrease_decorator
def get_number(): return 5 print(get_number())
>>> Increasing number by 2
Decreasing number by 1
6
Real-life Use Cases of Python Decorators
A very popular way of using decorators in Python is as a time logger. This helps a programmer know the amount of time a function takes to execute as a way of measuring efficiency.
Memoization is another cool way to use decorators in Python. Results from function calls that are repeated without any change can be easily remembered when performing calculations later. You can simply memoize a function with decorators.
Built-in Python decorators like @classmethod
(class method), @staticmethod
(static method), and @property
are very popular in Python’s OOP decorator pattern.
Conclusion
Python decorators enforce the DRY principle of software engineering because they serve as reusable code. Think of the many Python functions you could refactor to decorators. In this article, we’ve explored different forms of decorators. There are also class decorators, although we haven’t touched on those here.
Decorators make it easier to add additional functionality to a simple function, method or class without having to alter its source code while keeping your code DRY. Try decorating functions on your own to better understand the decorator pattern.