Decorators are often used for logging, enforcing access control, instrumentation, and caching. They leverage the concept of higher-order functions, where functions can accept other functions as arguments. This article will cover the basic syntax of decorators, how to create them, and some advanced use cases.

Basic Decorator Syntax

A decorator is a function that takes another function as an argument and extends its behavior. Here’s a simple example:

def simple_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

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

say_hello()

Output:

Something is happening before the function is called.
Hello!
Something is happening after the function is called.

In the example above, the simple_decorator function wraps the say_hello function, adding behavior before and after its execution.

Decorators with Arguments

Sometimes you may want to pass arguments to your decorators. This requires an additional level of nested functions. Here’s how you can achieve that:

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

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

greet("Alice")

Output:

Hello, Alice!
Hello, Alice!
Hello, Alice!

In this example, the repeat decorator takes an argument num_times, allowing the greet function to be called multiple times.

Using functools.wraps

When creating decorators, it’s a good practice to use functools.wraps. This decorator helps preserve the metadata of the original function, such as its name and docstring. Here’s an example:

from functools import wraps

def log_function_data(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper

@log_function_data
def add(a, b):
    """Returns the sum of a and b."""
    return a + b

print(add(5, 3))
print(add.__name__)  # Outputs: add
print(add.__doc__)   # Outputs: Returns the sum of a and b.

Output:

Calling function: add
add returned: 8
8
add
Returns the sum of a and b.

Chaining Decorators

You can also apply multiple decorators to a single function. They will be executed in the order they are applied, from the innermost to the outermost.

def uppercase_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@uppercase_decorator
@log_function_data
def say_hello():
    return "Hello!"

print(say_hello())

Output:

Calling function: say_hello
say_hello returned: HELLO!
HELLO!

Class Decorators

Decorators can also be applied to classes. This can be useful for modifying class behavior or adding class-level attributes.

def add_class_attribute(cls):
    cls.new_attribute = "This is a new attribute"
    return cls

@add_class_attribute
class MyClass:
    pass

print(MyClass.new_attribute)  # Outputs: This is a new attribute

Best Practices for Using Decorators

  1. Keep It Simple: Decorators should be simple and focused on a single task. This makes them easier to understand and maintain.
  2. Use functools.wraps: Always use functools.wraps to preserve the original function’s metadata.
  3. Document Your Decorators: Provide clear docstrings for your decorators to describe their purpose and usage.
  4. Test Decorators Thoroughly: Ensure that decorators do not introduce unintended side effects by writing unit tests.

Conclusion

Python decorators are a powerful tool for enhancing and modifying function behavior in a clean and efficient manner. By understanding their syntax and best practices, you can leverage decorators to write more readable, maintainable, and reusable code.


Learn more with useful resources