what-are-python-decorators-a-complete-guide-with-examples

What are Python Decorators? A Complete Guide with Examples

In this tutorial, you will learn what is a decorator and how to create your own custom decorators in Python.

A decorator is a special type of function in Python that is designed to wrap another function or class, adding extra functionality to it. It takes a function as an argument and returns a new function that includes the additional functionality.

Table of Contents

  1. First-Class Objects
  2. What are Decorators?
  3. How do Decorators Work?
  4. How to Create Decorators?
  5. Practical Examples of Decorators
  6. Chaining Decorators
  7. Conclusion

To apply a decorator to a function, you use the “@” symbol followed by the decorator’s name, which is placed on the line directly before the function’s definition. However, before we delve into decorators, it’s important to grasp a few fundamental concepts that will greatly assist in understanding decorators.

1. First-Class Objects

In Python, functions are considered first-class objects. This means that functions can be assigned to variables, passed as arguments to other functions, returned as values from functions, and stored in data structures.

Here are some key characteristics of first-class objects in Python:

1.1. Function assigning to variables

You can assign a function to a variable, just like any other object.

def greet():
    print("Hello!")

my_function = greet
my_function()  # Outputs: Hello!

1.2. Function passing as arguments

Functions can be passed as arguments to other functions.

def square(x):
    return x * x

def process(func, value):
    result = func(value)
    print("Processed value:", result)

process(square, 5)  # Outputs: Processed value: 25

1.3. Function returning as values

Functions can be returned as values from other functions.

def create_multiplier(factor):
    def multiplier(x):
        return x * factor
    return multiplier

double = create_multiplier(2)
print(double(5))  # Outputs: 10

1.4. Function storing in data structures

Functions can be stored in data structures like lists, dictionaries, or sets.

def add(x, y):
    return x + y

def subtract(x, y):
    return x - y

function_list = [add, subtract]

result = function_list[0](5, 3)
print(result)  # Outputs: 8

The ability to treat functions as first-class objects is a powerful feature of Python and enables the use of higher-order functions, function decorators, and other advanced programming techniques.

Now that we have explored the three examples above, which illustrate important concepts for understanding decorators, let’s delve deeper into the topic of decorators.


2. What are Decorators?

As stated above, Python decorators are a powerful and versatile feature that allows programmers to modify or enhance the behavior of functions or classes. Decorators provide a clean and concise way to add functionality to existing code without modifying its original implementation.

3. How do Decorators Work?

Under the hood, decorators are implemented as callable objects that take in a function or class as input and return a modified or wrapped version of the original. When a decorated function or class is called, it actually invokes the modified version with the additional functionality provided by the decorator.

4. How to Create Decorators?

Decorators can be created as either function decorators or class decorators.

4.1. How to Create Function Decorators?

Function decorators are the most common type of decorators. They are created using regular Python functions and are used to modify the behavior of other functions. Function decorators typically take the target function as an argument, modify it, and return the modified version.

To better understand function decorators, let’s break down the steps involved in creating and applying a function decorator:

1. Define the decorator function:

A decorator function is a regular Python function that takes a function as an argument and returns a modified version of that function. The decorator function usually defines an inner function, often called a “wrapper” that wraps the original function and adds the desired functionality.

The wrapper function can then be customized to perform any additional operations before or after the original function is called.

2. Apply the decorator:

To apply the decorator to a target function, we use the “@” symbol followed by the decorator name, placed directly above the function definition. This syntax is a shorthand way of applying the decorator to the function.

3. Modify the target function:

The decorator modifies the behavior of the target function by wrapping it with the wrapper function defined inside the decorator. This allows the decorator to add extra functionality, such as logging, caching, input validation, or performance monitoring, without modifying the original implementation of the target function.

Return the modified function:

The decorator returns the modified version of the target function, which incorporates the additional functionality defined in the wrapper function. This modified function can then be called as usual.

Let’s illustrate this with an example of a simple function decorator that adds a prefix to the output of a function:

def prefix_decorator(func):
    def wrapper():
        print("Prefix: [")
        func()
        print("]")
    return wrapper

@prefix_decorator
def print_message():
    print("Hello, World!")

print_message()

Upon executing the provided code, the expected output shall be:

Prefix: [
Hello, World!
]

4.2. How to Create Class Decorators?

Class decorators, as the name suggests, are used to modify or enhance the behavior of classes. They are created using regular Python classes. Class decorators receive the class as an argument, modify it, and return the modified version.

To understand class decorators, let’s break down the steps involved in creating and applying a class decorator:

1. Define the decorator class:

A class decorator is a regular Python class that takes a class as an argument and returns a modified version of that class. The decorator class defines a __call__ method, which allows the class instance to be callable like a function. Inside the __call__ method, you can modify the class as needed, add attributes or methods, or perform any other customization.

2. Apply the decorator:

To apply the class decorator to a target class, we use the “@” symbol followed by the decorator name, placed directly above the class definition. This syntax is a shorthand way of applying the decorator to the class.

Modify the target class:

The decorator modifies the behavior of the target class by invoking the __call__ method of the decorator class. Inside the __call__ method, you have access to the class and can modify it as desired. You can add new methods or attributes, override existing methods, or perform any other customization specific to the target class.

Return the modified class:

The decorator returns the modified version of the target class, which incorporates the changes made inside the __call__ method. This modified class can then be instantiated and used as usual.

Let’s illustrate this with an example of a simple class decorator that adds a new method to a target class:

class AddMethodDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        self.func(*args, **kwargs)

        def new_method(self):
            print("This is a new method added by the decorator!")

        setattr(self.func, "new_method", new_method)
        return self.func(*args, **kwargs)

@AddMethodDecorator
class MyClass:
    def original_method(self):
        print("This is the original method.")

obj = MyClass()
obj.original_method()
obj.new_method()

Upon executing the provided code, the expected output shall be:

This is the original method.
This is a new method added by the decorator!

4.3. Important built-in decorators: @staticmethod, @classmethod, and @property

1. @staticmethod decorator

This decorator is used to declare a static method inside a class. Static methods don’t have access to the instance or class and can be called on the class itself. Here’s an example:

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

result = MathUtils.add(5, 3)
print(result)  # Output: 8

2. @classmethod decorator

This decorator is used to declare a class method. Class methods receive the class as the first argument with the name cls instead of the instance. They can be called on both the class and instances of the class. Here’s an example:

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    @classmethod
    def from_diameter(cls, diameter):
        radius = diameter / 2
        return cls(radius)

circle = Circle.from_diameter(10)
print(circle.radius)  # Output: 5.0

3. @property decorator

This decorator is used to define a method as a property, allowing it to be accessed like an attribute without using parentheses. It can be used to implement getters, setters, and deleters for class attributes. Here’s an example:

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height
    
    @property
    def width(self):
        return self._width
    
    @width.setter
    def width(self, value):
        if value > 0:
            self._width = value
    
    @property
    def height(self):
        return self._height
    
    @height.setter
    def height(self, value):
        if value > 0:
            self._height = value

rectangle = Rectangle(5, 3)
print(rectangle.width)  # Output: 5
rectangle.width = 7
print(rectangle.width)  # Output: 7

These are just a few examples of built-in decorators in Python. There are also other useful decorators like @functools.wraps for preserving function metadata, @abstractmethod for defining abstract methods in abstract base classes, and more.


5. Practical Examples of Decorators

In this section, we will explore a few practical examples of decorators to demonstrate their usefulness.

5.1. Create a Logging Decorator to log information when a function is called

A logging decorator can be used to log information about when a function is called and what arguments it receives. This can be handy for debugging or understanding the flow of execution in a program.

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        print(f"Arguments: {args}, {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_decorator
def greet(name):
    print(f"Hello from, {name}!")

greet("VPK Technologies")

Upon executing the provided code, the expected output shall be:

Calling function: greet
Arguments: (' VPK Technologies ',), {}
Hello from, VPK Technologies!

5.2. Create a Timing Decorator to measure the execution time of the function

A timing decorator can be used to measure the execution time of a function. It can be useful for profiling or optimizing code.

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f"Execution time: {execution_time} seconds")
        return result
    return wrapper

@timing_decorator
def calculate_sum(n):
    total = 0
    for i in range(1, n+1):
        total += i
    return total

print("Sum is:", calculate_sum(100000))

Upon executing the provided code, the expected output shall be:

Execution time: 0.005623817443847656 seconds
Sum is: 5000050000

5.3 Create an Authorization Decorator to add an authentication check before executing a function

An authorization decorator can be used to add an authentication check before executing a function. It can ensure that only authorized users have access to sensitive operations.

def authorize_decorator(func):
    def wrapper(*args, **kwargs):
        if is_user_authenticated():
            return func(*args, **kwargs)
        else:
            raise Exception("Unauthorized access!")
    return wrapper

@authorize_decorator
def delete_file(file_path):
    # Code to delete the file
    print(f"File {file_path} deleted.")

delete_file("/path/to/file.txt")

Upon executing the provided code, the expected output shall be:

Unauthorized access!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in wrapper
Exception: Unauthorized access!

6. Chaining Decorators

Chaining decorators is a powerful feature in Python that allows you to apply multiple decorators to a single function or class. This enables you to compose different functionalities and enhance the behavior of your code. Let’s explore chaining decorators with an example.

def decorator1(func):
    def wrapper():
        print("Decorator 1: Before function execution")
        func()
        print("Decorator 1: After function execution")
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2: Before function execution")
        func()
        print("Decorator 2: After function execution")
    return wrapper

@decorator1
@decorator2
def my_function():
    print("Inside my_function")

my_function()

Upon executing the provided code, the expected output shall be:

Decorator 1: Before function execution
Decorator 2: Before function execution
Inside my_function
Decorator 2: After function execution
Decorator 1: After function execution

Chaining decorators allow you to combine different functionalities and customize the behavior of your functions or classes in a modular and reusable manner. By applying multiple decorators in a specific order, you can build complex behavior by composing simpler decorators. This promotes code organization and separation of concerns, making your code more maintainable and flexible.

7. Conclusion

Decorators are a powerful tool in Python that enable us to enhance the behavior of functions and classes without modifying their original implementation. They provide a clean and concise way to add functionality such as logging, timing, authorization, and more. By using decorators, we can write reusable and modular code that is easy to maintain and extend.

Related: See our guide on How the input() function Works in Python?

Scroll to Top