Python Decorators

basic
Published

June 15, 2024

Python decorators are a powerful and expressive feature that allows you to modify functions and methods in a clean and readable way. They provide a concise syntax for wrapping additional functionality around an existing function without modifying its core behavior. This blog post will look at decorators in detail, providing clear explanations and practical examples.

Understanding the Basics

At its heart, a decorator is a higher-order function—a function that takes another function as an argument and returns a modified version of that function. Let’s start with a simple example:

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

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

say_hello()

This code defines a decorator my_decorator. The @my_decorator syntax above say_hello is syntactic sugar; it’s equivalent to:

say_hello = my_decorator(say_hello)

When say_hello() is called, it first executes the code within wrapper(), printing messages before and after the original say_hello() function. This demonstrates the basic principle: the decorator wraps additional functionality around the original function.

Decorators with Arguments

The previous example showed a decorator without arguments. Let’s see how to handle decorators that need to accept arguments:

def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"Hello, {name}!")

greet("World")

Here, repeat is a decorator factory; it returns a decorator that repeats the decorated function a specified number of times. Note the use of *args and **kwargs in wrapper to handle functions with various arguments.

Decorators with Return Values

Decorators can also handle functions that return values:

def bold_decorator(func):
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

@bold_decorator
def get_message():
    return "Hello, World!"

print(get_message()) # Output: <b>Hello, World!</b>

This example shows how a decorator can modify the return value of the decorated function, adding HTML bold tags in this case.

Using functools.wraps

When using decorators, it’s important to preserve the metadata of the original function. The wraps decorator from the functools module helps with this:

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@my_decorator
def say_hello():
    """This is a simple function."""
    print("Hello!")

print(say_hello.__name__)  # Output: say_hello (Preserves the name)
print(say_hello.__doc__)   # Output: This is a simple function. (Preserves the docstring)

Without wraps, the __name__ and __doc__ attributes would refer to the wrapper function, not the original say_hello function. wraps ensures the original function’s metadata is preserved.

Practical Applications

Decorators are widely used in various scenarios, including:

  • Logging: Record function calls and their arguments.
  • Timing: Measure the execution time of functions.
  • Authentication: Check user permissions before executing a function.
  • Caching: Store the results of expensive function calls to improve performance.
  • Input validation: Validate the input arguments of a function.

By mastering Python decorators, you can write more efficient, reusable, and elegant code. They offer a powerful mechanism to improve your functions without cluttering your codebase.