Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

51. What are Python decorators? How do you create and use them?

Python decorators are a powerful and elegant way to modify or enhance functions or methods without permanently changing their actual code. They essentially "wrap" another function, allowing you to execute code before, after, or even around the wrapped function's execution. Decorators are often used for tasks like logging, access control, timing, caching, and more.

Syntactically, a decorator is applied using the @decorator_name syntax placed directly above the function definition you want to decorate.

How Decorators Work:

At its core, a decorator is just a function that takes another function as an argument, adds some functionality, and returns a new function (or modifies the original one).

When you write:

@my_decorator
def my_function():
    pass

This is just syntactic sugar for:

def my_function():
    pass
my_function = my_decorator(my_function)

So, my_decorator is called with my_function as its argument, and whatever my_decorator returns replaces the original my_function .

Creating Decorators:

Decorators can be created as either functions or classes.

1. Function-Based Decorators:

These are the most common. A function-based decorator typically follows this pattern:

import functools

def my_decorator(func):
    @functools.wraps(func) # Important for preserving original function metadata
    def wrapper(*args, **kwargs):
        # Code to run BEFORE the decorated function
        print(f"Calling function: {func.__name__}")
        result = func(*args, **kwargs) # Call the original function
        # Code to run AFTER the decorated function
        print(f"Function {func.__name__} finished.")
        return result
    return wrapper # Return the new wrapper function
  • my_decorator takes func (the function to be decorated) as an argument.
  • It defines an inner function, wrapper , which is what will actually be called when the decorated function is invoked.
  • wrapper contains the extra logic (e.g., printing messages) and calls the original func .
  • functools.wraps(func) is crucial. It copies the __name__ , __doc__ , and other attributes from the original function ( func ) to the wrapper function. Without it, debugging would be harder as the decorated function would appear to be named wrapper .
  • my_decorator returns this wrapper function.

2. Class-Based Decorators:

A class can also act as a decorator if it implements the __init__ and __call__ special methods.

  • __init__ receives the function to be decorated.
  • __call__ defines what happens when the decorated function (which is now an instance of the decorator class) is invoked.
import functools

class MyDecoratorClass:
    def __init__(self, func):
        self.func = func
        functools.wraps(func)(self) # Preserve metadata
        print(f"MyDecoratorClass initialized for function: {func.__name__}")

    def __call__(self, *args, **kwargs):
        # Code to run BEFORE the decorated function
        print(f"Calling function from class decorator: {self.func.__name__}")
        result = self.func(*args, **kwargs) # Call the original function
        # Code to run AFTER the decorated function
        print(f"Function {self.func.__name__} finished (class decorator).")
        return result

Using Decorators:

Once defined, you apply them with the @ syntax:

# Using the function-based decorator
@my_decorator
def greet(name):
    return f"Hello, {name}!"

# Using the class-based decorator
@MyDecoratorClass
def farewell(name):
    return f"Goodbye, {name}!"

print(greet("Alice"))
print(farewell("Bob"))

Why Use Decorators?

  • Code Reusability: Apply the same functionality to multiple functions without repeating code.
  • Separation of Concerns: Keep core business logic separate from auxiliary concerns (like logging, authentication).
  • Readability: The @ syntax is concise and clearly indicates that a function's behavior is being enhanced.
  • Frameworks and Libraries: Widely used in web frameworks (e.g., Flask, Django for routing, authentication), testing frameworks, and more.

Decorators are a powerful and common pattern in Python, allowing for clean, modular, and maintainable code by neatly packaging reusable code enhancements.

Example: Implementing and Using Decorators

import time
import functools

# --- 1. Function-based Decorator: Timer ---
print("--- Function-based Decorator: Timer ---")

def timer(func):
    """A decorator that measures the execution time of a function."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"'{func.__name__}' took {run_time:.4f} seconds to execute.")
        return result
    return wrapper

@timer
def long_running_task(n):
    """Performs a calculation that takes some time."""
    total = 0
    for i in range(n):
        total += i * i
    return total

@timer
def simulate_api_call(delay):
    """Simulates a network API call with a given delay."""
    time.sleep(delay)
    return "API call completed"

print(f"Result of long_running_task: {long_running_task(1_000_000)}")
print(f"Result of simulate_api_call: {simulate_api_call(0.5)}")

# Verify metadata is preserved
print(f"Name of long_running_task: {long_running_task.__name__}")
print(f"Docstring of long_running_task: {long_running_task.__doc__}")


# --- 2. Class-based Decorator: Logger ---
print("\n--- Class-based Decorator: Logger ---")

class Logger:
    """A class-based decorator that logs function calls."""
    def __init__(self, func):
        self.func = func
        functools.wraps(func)(self) # Essential for preserving metadata
        print(f"Logger initialized for '{func.__name__}'")

    def __call__(self, *args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"LOG: Calling '{self.func.__name__}' with args ({signature})")

        result = self.func(*args, **kwargs)

        print(f"LOG: '{self.func.__name__}' returned: {result!r}")
        return result

@Logger
def add_numbers(a, b):
    return a + b

@Logger
def greet_person(name, title="Mr/Ms"):
    return f"Hello, {title}. {name}!"

print(f"\nResult of add_numbers: {add_numbers(10, 20)}")
print(f"Result of greet_person: {greet_person('Alice', title='Dr')}")

# Verify metadata is preserved
print(f"Name of add_numbers: {add_numbers.__name__}")
print(f"Docstring of add_numbers: {add_numbers.__doc__}")


# --- 3. Decorator with Arguments: Permissions Checker ---
print("\n--- Decorator with Arguments: Permissions Checker ---")

def requires_permission(permission_level):
    """
    A decorator factory that returns a decorator to check user permissions.
    This is a decorator that takes arguments, so it's a "decorator factory".
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(user_role, *args, **kwargs):
            # Assume a simple permission check for demonstration
            if user_role == permission_level or user_role == "admin":
                print(f"PERMISSION CHECK: User '{user_role}' has '{permission_level}' access. Running '{func.__name__}'.")
                return func(user_role, *args, **kwargs)
            else:
                print(f"PERMISSION DENIED: User '{user_role}' does not have '{permission_level}' access for '{func.__name__}'.")
                return "ACCESS DENIED"
        return wrapper
    return decorator

@requires_permission("user")
def view_dashboard(user_role):
    return f"Dashboard data for {user_role}."

@requires_permission("admin")
def delete_critical_data(user_role, item_id):
    return f"CRITICAL: Deleting item {item_id} by {user_role}."

print(f"\nUser 'user' trying to view dashboard: {view_dashboard('user')}")
print(f"User 'guest' trying to view dashboard: {view_dashboard('guest')}")

print(f"\nUser 'user' trying to delete data: {delete_critical_data('user', 123)}")
print(f"User 'admin' trying to delete data: {delete_critical_data('admin', 456)}")

Explanation of the Example Code:

  • Function-based Decorator: timer
    • The timer function takes another function ( func ) as input.
    • It defines a wrapper function inside, which wraps the original func .
    • The wrapper calculates the execution time of func and prints it, then returns the result of func .
    • @functools.wraps(func) is used to ensure that the decorated function ( long_running_task or simulate_api_call ) retains its original name, docstring, and other metadata, which is vital for introspection and debugging.
  • Class-based Decorator: Logger
    • The Logger class is initialized with the function to be decorated ( func ).
    • The __call__ method of the Logger instance is executed whenever the decorated function is called.
    • It logs the arguments passed to the function before execution and the return value after execution.
    • This demonstrates how a class can act as a decorator by implementing __init__ and __call__ . Again, functools.wraps is important.
  • Decorator with Arguments: requires_permission
    • Sometimes you need a decorator to take arguments (e.g., permission_level ). To achieve this, you create a "decorator factory" – a function ( requires_permission ) that takes the arguments for the decorator and returns the actual decorator function.
    • The inner decorator function then takes the func to be decorated, and it, in turn, returns the wrapper function.
    • The wrapper function contains the logic to check permissions based on the permission_level captured from the outer scope, before deciding whether to execute the original func . This allows you to apply the same permission logic with different levels.