Python decorators are a powerful feature of the language that allows you to modify or extend the behavior of a function or a class without modifying its source code. A decorator is basically a function that takes another function or class as an argument and returns a new function or class that can be used instead of the original one.

SecurePZ 📷 Photo: Stable Diffusion

In this tutorial, I will cover the following topics:

  • What are decorators in Python?
  • How to define and use decorators?
  • Commonly used Built-in decorators
  • Examples of decorators

What are decorators in Python?

Decorators are functions that take another function as an argument and return a new function. The new function is a modified version of the original function, with additional behavior added by the decorator. This makes them a powerful tool for separating concerns and keeping code modular.

How to use decorators?

We can simply define a function that takes another function as an argument and returns a new function that has the additional behavior. Here’s a simple example:

1
2
3
4
5
6
def my_decorator(func):
    def wrapper():
        print("Before the function is called.")
        func()
        print("After the function is called.")
    return wrapper

This my_decorator function calls the fun inside its wrapper.

1
2
3
4
5
def say_hello():
    print("Hello!")

say_hello = my_decorator(say_hello)
say_hello()

The code above is the same as:

1
2
3
4
5
@my_decorator
def say_hello():
    print("Hello!")

say_hello()

Both will output:

1
2
3
Before the function is called.
Hello!
After the function is called.

In this example, my_decorator is a function that takes func and returns a new function wrapper that has additional behavior, which is printing two lines before and after calling the passed original function.

We then use the decorator by calling my_decorator with the say_hello function as an argument or by using the @my_decorator syntax and assigning the result to say_hello. We can then call say_hello as usual, and the decorator’s behavior will be added.

Surely using @ is the preferred form. Simply put we read this block:

@my_decorator
def say_hello():

as:

Pass say_hello to my_decorator and replace it with the returned version.

Examples of decorators

Let’s look at some more examples of decorators in Python.

  1. Simple decorator:
1
2
3
4
5
@my_decorator
def say_hello():
    print("Hello!")

say_hello()
  1. Decorator with arguments:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def repeat(num):
    def my_decorator(func):
        def wrapper():
            for i in range(num):
                func()
        return wrapper
    return my_decorator

@repeat(num=3)
def say_hello():
    print("Hello!")

say_hello()

In this example, we define a decorator that takes an argument num and returns a decorator function that takes another function as an argument and returns a new function that repeats the original function num times. We use the decorator by calling repeat(num=3) to get the decorator function, and then applying it to say_hello using the @ syntax.

output:

1
2
3
Hello!
Hello!
Hello!
  1. Class decorator
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class my_decorator:
    def __init__(self, func):
        self.func = func
    def __call__(self):
        print("Before the function is called.")
        self.func()
        print("After the function is called.")

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

We can also define our decorator as a class. Then we need to assign it to the class in its constructor. The output is the same as the second example.

Built-in Decorators

Python has several decorators that can be used to modify methods. Here are a few important ones:

  1. @property:

the @property decorator is used to define a method as a property of a class. When a method is decorated with @property, it can be accessed like an attribute, without the need to call the method explicitly.

In other words, @property allows you to define a method as if it were an attribute of the class, which makes it easier to use and more intuitive for other developers who may be using your code.

Here is an example of how to use @property:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def diameter(self):
        return self.radius * 2

    @diameter.setter
    def diameter(self, value):
        self.radius = value / 2

circle = Circle(5)
print(circle.radius)  # Output: 5
print(circle.diameter)  # Output: 10

circle.diameter = 20
print(circle.radius)  # Output: 10
print(circle.diameter)  # Output: 20

In this example, we define a Circle class that has a radius attribute and a diameter property, which is calculated based on the radius. The diameter property is defined using @property, which means that it can be accessed like an attribute.

We also define a diameter setter method, which allows us to set the diameter and automatically updates the radius based on the new value. This is done using the @diameter.setter decorator, which is used to define a method that sets the value of the diameter property.

When we create an instance of the Circle class and access the diameter property, it calculates the diameter based on the radius. When we set the diameter property, it updates the radius and recalculates the diameter.

  1. @staticmethod:

This decorator is used to define a static method in a class. A static method is a method that belongs to a class rather than an instance of a class.

1
2
3
4
5
6
class MyClass:
    @staticmethod
    def my_method():
        print("This is a static method.")

MyClass.my_method() # Output: "This is a static method."
  1. @classmethod:

This decorator is used to define a class method in a class. A class method is a method that takes the class itself as its first argument, rather than an instance of the class.

1
2
3
4
5
6
7
8
class MyClass:
    class_attribute = "class attribute"

    @classmethod
    def my_method(cls):
        print(f"This is a class method. The class attribute is {cls.class_attribute}.")

MyClass.my_method() # Output: "This is a class method. The class attribute is class attribute."
  1. @abstractmethod:

This decorator is used to define an abstract method in an abstract base class. An abstract method is a method that is defined in the base class but has no implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from abc import ABC, abstractmethod

class MyAbstractClass(ABC):
    @abstractmethod
    def my_method(self):
        pass

class MyClass(MyAbstractClass):
    def my_method(self):
        print("This is a concrete implementation of the abstract method.")

my_class = MyClass()
my_class.my_method() # Output: "This is a concrete implementation of the abstract method."
  1. @lru_cache:

This decorator is used to cache the result of a function call, so that the next time the function is called with the same arguments, the cached result is returned instead of re-evaluating the function.

1
2
3
4
5
6
7
8
9
from functools import lru_cache

@lru_cache(maxsize=None)
def my_function(argument):
    print("This function is being evaluated.")
    return argument

print(my_function(1)) # Output: "This function is being evaluated.", "1"
print(my_function(1)) # Output: "1"

These are just a few examples of the many decorators available in Python. Decorators can be a powerful tool for modifying the behavior of functions and methods, and can help make your code more modular and easier to read.