Python > Advanced Topics and Specializations > Meta-programming > Decorators

Simple Decorator Example: Timing Function Execution

This example demonstrates a basic decorator that measures and prints the execution time of a function. Decorators are a powerful feature in Python that allows you to modify the behavior of functions or methods in a clean and readable way.

Concepts Behind Decorators

Decorators are essentially syntactic sugar for wrapping a function with another function. They use the @ symbol followed by the decorator name to apply the wrapper. The decorator function takes the original function as an argument, extends its behavior (e.g., by adding logging or timing), and returns the modified function. This allows you to separate concerns and avoid code duplication.

The Timing Decorator

The timer function is our decorator. It takes a function func as input. Inside timer, we define a wrapper function that will be executed when the decorated function is called. The wrapper measures the execution time before and after calling the original function func. It then prints the execution time and returns the result of the original function. Finally, the timer function returns the wrapper function. The @timer syntax above my_function is equivalent to writing my_function = timer(my_function).

import time

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

@timer
def my_function(n):
    time.sleep(n)

my_function(2)

Real-Life Use Case

Timing function execution is useful for identifying performance bottlenecks in your code. You can use this decorator to measure how long different parts of your application take to run, allowing you to focus your optimization efforts on the areas that will have the biggest impact. Other common use cases include logging, authentication, input validation, and caching.

Best Practices

  • Use functools.wraps to preserve the original function's metadata (name, docstring, etc.).
  • Keep decorators concise and focused on a single responsibility.
  • Consider using decorator factories (decorators that take arguments) for greater flexibility.

Interview Tip

Be prepared to explain how decorators work under the hood. Understand the difference between decorating a function with @my_decorator and manually applying the decorator using my_function = my_decorator(my_function). Also, be able to write a simple decorator from scratch.

When to Use Them

Use decorators when you need to add common functionality to multiple functions or methods without modifying their original code. They promote code reuse and improve readability by separating concerns. Decorators are well-suited for tasks like logging, timing, authorization, and validation.

Memory Footprint

Decorators generally have a minimal impact on memory footprint. They introduce a small overhead due to the additional function call (the wrapper). However, the benefits of code reuse and improved readability often outweigh this minor cost. Avoid creating excessively complex decorators that perform resource-intensive operations.

Alternatives

Alternatives to decorators include using inheritance, mixins, or manual function wrapping. However, decorators are often the most elegant and concise solution for adding cross-cutting concerns to functions.

Pros

  • Improved code readability and maintainability.
  • Code reuse and reduced duplication.
  • Separation of concerns.
  • Clean syntax.

Cons

  • Can make debugging more difficult if used excessively or incorrectly.
  • May introduce a slight performance overhead.
  • Requires understanding of higher-order functions and closures.

FAQ

  • What is the difference between a decorator and a decorator factory?

    A decorator is a function that takes another function as an argument and returns a modified function. A decorator factory is a function that returns a decorator. Decorator factories allow you to parameterize decorators with arguments.
  • How do I preserve the original function's metadata when using a decorator?

    Use the functools.wraps decorator from the functools module. This decorator copies the original function's metadata (name, docstring, etc.) to the wrapper function.