Understanding Decorators

A decorator is a function that takes another function as an argument, extends its behavior, and returns a new function. The syntax for decorators involves the @decorator_name syntax placed above the function definition.

Basic Decorator Example

Here’s a simple example of a decorator that adds a greeting to a function:

def greet_decorator(func):
    def wrapper(*args, **kwargs):
        print("Hello!")
        return func(*args, **kwargs)
    return wrapper

@greet_decorator
def say_name(name):
    print(f"My name is {name}")

say_name("Alice")

Output

Hello!
My name is Alice

Advanced Decorator Use Cases

1. Logging Decorator

Logging is essential for debugging and monitoring applications. A logging decorator can help you track function calls and their arguments.

import logging

logging.basicConfig(level=logging.INFO)

def log_decorator(func):
    def wrapper(*args, **kwargs):
        logging.info(f"Calling function '{func.__name__}' with arguments {args} and {kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"Function '{func.__name__}' returned {result}")
        return result
    return wrapper

@log_decorator
def add(a, b):
    return a + b

add(5, 3)

Output

INFO:root:Calling function 'add' with arguments (5, 3) and {}
INFO:root:Function 'add' returned 8

2. Access Control Decorator

You can use decorators to enforce access control on functions, ensuring that only authorized users can execute certain functions.

def requires_permission(permission):
    def decorator(func):
        def wrapper(user_permissions, *args, **kwargs):
            if permission not in user_permissions:
                raise PermissionError(f"User does not have {permission} permission.")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@requires_permission('admin')
def delete_user(user_id):
    print(f"User {user_id} deleted.")

# Simulating user permissions
try:
    delete_user(['user'], 123)  # This will raise an exception
except PermissionError as e:
    print(e)

delete_user(['admin'], 123)  # This will work

Output

User does not have admin permission.
User 123 deleted.

3. Performance Measurement Decorator

Performance measurement is crucial in optimizing code. A decorator can be employed to measure the execution time of functions.

import time

def time_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function '{func.__name__}' executed in {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@time_decorator
def compute_factorial(n):
    if n == 0:
        return 1
    return n * compute_factorial(n - 1)

compute_factorial(5)

Output

Function 'compute_factorial' executed in 0.0000 seconds

Chaining Decorators

Decorators can be stacked to apply multiple enhancements to a single function. The order of decorators matters, as they are applied from the innermost to the outermost.

@log_decorator
@time_decorator
def multiply(a, b):
    return a * b

multiply(5, 7)

Output

INFO:root:Calling function 'multiply' with arguments (5, 7) and {}
Function 'multiply' executed in 0.0000 seconds
INFO:root:Function 'multiply' returned 35

Summary of Decorator Use Cases

Use CaseDescriptionExample Function
LoggingTracks function calls and their argumentslog_decorator
Access ControlRestricts function access based on user permissionsrequires_permission
Performance MeasurementMeasures the execution time of functionstime_decorator

Best Practices for Writing Decorators

  1. Use functools.wraps: When creating decorators, use functools.wraps to preserve the metadata of the original function.
   from functools import wraps

   def log_decorator(func):
       @wraps(func)
       def wrapper(*args, **kwargs):
           ...
       return wrapper
  1. Keep Decorators Simple: Decorators should be focused on a single responsibility. If a decorator is doing too much, consider breaking it down.
  1. Document Your Decorators: Provide clear documentation for your decorators, explaining their purpose and usage.
  1. Test Your Decorators: Ensure that your decorators are well-tested, especially if they modify the behavior of critical functions.

By leveraging decorators effectively, you can enhance your Python applications, making them more modular and maintainable.

Learn more with useful resources