Decorators in Python

Decorators in Python

This article explains decorators in Python.

YouTube Video

Decorators in Python

Python decorators are a powerful feature used to add additional functionality to functions or methods. Decorators allow you to add new functionality without modifying existing code, thereby improving code reusability and maintainability.

Basics of Decorators

Python decorators work by taking a function as an argument and returning a new function with added functionality. Using decorators, you can easily add pre-processing or post-processing to functions.

Without decorators, altering a function's behavior requires directly editing the function itself. With decorators, you can extend functionality without modifying the original implementation.

Basic Structure of a Decorator

 1def my_decorator(func):
 2    def wrapper():
 3        print("Before the function call")
 4        func()
 5        print("After the function call")
 6    return wrapper
 7
 8@my_decorator
 9def say_hello():
10    print("Hello!")
11
12say_hello()
  • In this example, the my_decorator function serves as a decorator, wrapping the say_hello function. The decorator is applied using the @my_decorator syntax, and when say_hello() is called, the pre- and post-processing provided by the decorator is automatically executed.

How Decorators Work

Decorators work in the following steps.

  1. The decorator takes a function (or method) as an argument.

  2. It defines a wrapper function to execute the original function.

  3. The wrapper function performs additional processing before or after executing the original function.

  4. The decorator returns the wrapper function. As a result, the original function is replaced with a new, decorated function.

Decorators for Functions with Arguments

When applying a decorator to a function with arguments, the wrapper function must be adjusted to accept those arguments.

 1def my_decorator(func):
 2    def wrapper(*args, **kwargs):
 3        print("Before the function call")
 4        result = func(*args, **kwargs)
 5        print("After the function call")
 6        return result
 7    return wrapper
 8
 9@my_decorator
10def greet(name):
11    print(f"Hello, {name}!")
12
13greet("Alice")
  • By using *args and **kwargs, it is possible to handle functions that accept any number of arguments and keyword arguments. This allows decorators to be applied generically to functions with any type of arguments.

Examples of Applying Decorators

Decorator for Logging

Decorators are often used to add logging functionality. For example, by creating a decorator that logs before and after executing a function, you can record when the function is called and how long it takes to execute.

 1import time
 2
 3def log_time(func):
 4    def wrapper(*args, **kwargs):
 5        start_time = time.time()
 6        print(f"Calling function '{func.__name__}'...")
 7        result = func(*args, **kwargs)
 8        end_time = time.time()
 9        print(f"Function '{func.__name__}' completed in {end_time - start_time} seconds")
10        return result
11    return wrapper
12
13@log_time
14def long_task(duration):
15    time.sleep(duration)
16    print("Task completed!")
17
18long_task(2)
  • In this example, the log_time decorator measures a function's execution time and outputs it as a log. The long_task function is wrapped with the decorator, and its execution time is recorded during runtime.

Decorator for Permission Management

Decorators can also be used for permission management. For example, you can restrict processing by checking if a user has specific permissions.

 1def requires_permission(user_role):
 2    def decorator(func):
 3        def wrapper(*args, **kwargs):
 4            if user_role != "admin":
 5                print("Permission denied!")
 6                return
 7            return func(*args, **kwargs)
 8        return wrapper
 9    return decorator
10
11@requires_permission("admin")
12def delete_user(user_id):
13    print(f"User {user_id} has been deleted.")
14
15delete_user(123)  # Executed
16delete_user = requires_permission("guest")(delete_user)
17delete_user(456)  # Permission denied!
  • The requires_permission decorator restricts the execution of a function based on a user's role. By consolidating such logic into a decorator, you can prevent permission management logic from being scattered throughout your code, enhancing readability.

Nesting Decorators

It is possible to apply multiple decorators. When multiple decorators are applied to a single function, they execute in order from top to bottom.

 1def uppercase(func):
 2    def wrapper(*args, **kwargs):
 3        result = func(*args, **kwargs)
 4        return result.upper()
 5    return wrapper
 6
 7def exclaim(func):
 8    def wrapper(*args, **kwargs):
 9        result = func(*args, **kwargs)
10        return result + "!"
11    return wrapper
12
13@uppercase
14@exclaim
15def greet(name):
16    return f"Hello, {name}"
17
18print(greet("Alice"))  # "HELLO, ALICE!"
  • In this example, two decorators are applied to the greet function. The @exclaim decorator adds an exclamation mark to the end of the string, and then @uppercase converts the string to uppercase.

Decorators for Class Methods

Decorators can also be used with class methods. This is particularly useful when you want to control the behavior of methods within a class. When applying decorators to class methods, you need to be mindful of arguments like self or cls.

 1def log_method_call(func):
 2    def wrapper(self, *args, **kwargs):
 3        print(f"Calling method '{func.__name__}'...")
 4        return func(self, *args, **kwargs)
 5    return wrapper
 6
 7class MyClass:
 8    @log_method_call
 9    def greet(self, name):
10        print(f"Hello, {name}")
11
12obj = MyClass()
13obj.greet("Bob")
  • In this example, the log_method_call decorator is applied to the greet method to output a log when the method is called.

Cautions with Decorators

There are some considerations when using decorators. Decorators can potentially change the original function's name and documentation string (e.g., __name__ or __doc__), so it is recommended to use functools.wraps to preserve this information.

 1import functools
 2
 3# Decorator without functools.wraps
 4def bad_decorator(func):
 5    def wrapper(*args, **kwargs):
 6        print("Before the function call")
 7        return func(*args, **kwargs)
 8    return wrapper
 9
10# Decorator with functools.wraps
11def good_decorator(func):
12    @functools.wraps(func)
13    def wrapper(*args, **kwargs):
14        print("Before the function call")
15        return func(*args, **kwargs)
16    return wrapper
17
18# Apply decorators
19@bad_decorator
20def my_function_bad():
21    """This is the original docstring for my_function_bad."""
22    print("Executing my_function_bad")
23
24@good_decorator
25def my_function_good():
26    """This is the original docstring for my_function_good."""
27    print("Executing my_function_good")
28
29# Run functions
30print("=== Without functools.wraps ===")
31my_function_bad()
32print("Function name:", my_function_bad.__name__)
33print("Docstring:", my_function_bad.__doc__)
34
35print("\n=== With functools.wraps ===")
36my_function_good()
37print("Function name:", my_function_good.__name__)
38print("Docstring:", my_function_good.__doc__)
  • Using @functools.wraps ensures that the original function's metadata is correctly passed to the wrapper function.

Summary

Python decorators are a very powerful tool that allows you to concisely add additional functionality to functions and methods. They can be used in various scenarios to reduce code duplication and improve maintainability and reusability. By understanding how decorators work, you can write more efficient and flexible code.

You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.

YouTube Video