Hone logo
Hone
Problems

Mastering Python Decorators: Enhancing Functionality with Elegance

Python decorators are a powerful and elegant way to modify or enhance functions and methods. They allow you to wrap functions with other functions, enabling you to add pre-processing, post-processing, logging, access control, and more without altering the original function's code. This challenge will guide you through understanding and implementing various decorator patterns.

Problem Description

Your task is to implement several Python decorators to demonstrate your understanding of their functionality and common use cases. You will be provided with a series of functions, and you need to create decorators that can be applied to them to add specific behaviors.

Key Requirements:

  1. Basic Decorator Implementation: Create a decorator that logs the execution of a function, including its name and arguments.
  2. Decorator with Arguments: Implement a decorator that accepts arguments, allowing for customizable behavior (e.g., setting a prefix for log messages).
  3. Class-Based Decorator: Create a class that acts as a decorator. This class should also log function execution.
  4. Decorator Chaining: Demonstrate how to apply multiple decorators to a single function and ensure they execute in the correct order.
  5. Preserving Function Metadata: Ensure that your decorators preserve the original function's name, docstring, and other metadata.

Expected Behavior:

  • When a decorated function is called, the decorator's logic should execute before and/or after the original function's code, as specified by the decorator.
  • Any arguments passed to the decorated function should be correctly handled by the decorator and passed to the original function.
  • The return value of the original function should be correctly returned by the decorated function.
  • Metadata (name, docstring) of the original function should be accessible through the decorated function.

Edge Cases:

  • Functions with no arguments.
  • Functions with arbitrary positional and keyword arguments (*args, **kwargs).
  • Decorators applied to methods within a class.

Examples

Example 1: Basic Logging Decorator

Consider a simple function:

def say_hello(name):
    """Greets a person."""
    return f"Hello, {name}!"

We want to create a decorator log_execution that logs a message before and after the function call.

Input: Apply log_execution to say_hello and call it:

@log_execution
def say_hello(name):
    """Greets a person."""
    return f"Hello, {name}!"

print(say_hello("Alice"))

Output:

INFO: Executing say_hello with args ('Alice',), kwargs {}
INFO: Finished say_hello in 0.00... seconds.
Hello, Alice!

Explanation: The log_execution decorator wraps say_hello. When say_hello("Alice") is called, the decorator first prints an "Executing" message, then calls the original say_hello function, and finally prints a "Finished" message before returning the result.

Example 2: Decorator with Arguments

Let's create a decorator log_with_prefix that takes a prefix string.

Input: Apply log_with_prefix("CUSTOM_LOG") to a function and call it:

@log_with_prefix("CUSTOM_LOG")
def add_numbers(a, b):
    """Adds two numbers."""
    return a + b

print(add_numbers(5, 3))

Output:

CUSTOM_LOG: Executing add_numbers with args (5, 3), kwargs {}
CUSTOM_LOG: Finished add_numbers in 0.00... seconds.
8

Explanation: The log_with_prefix decorator takes an argument "CUSTOM_LOG". This argument is used to prepend messages logged by the decorator when add_numbers is called.

Example 3: Decorator Chaining and Metadata Preservation

Consider chaining two decorators: repeat(n) which repeats a function's execution n times, and log_execution.

Input: Apply both decorators and call the function:

@repeat(2)
@log_execution
def greet_user(user_id):
    """Fetches and greets a user."""
    print(f"Fetching user {user_id}...")
    return f"User {user_id} logged in."

print(greet_user(101))
print(f"Docstring: {greet_user.__doc__}")
print(f"Name: {greet_user.__name__}")

Output:

INFO: Executing greet_user with args (101,), kwargs {}
Fetching user 101...
INFO: Finished greet_user in 0.00... seconds.
INFO: Executing greet_user with args (101,), kwargs {}
Fetching user 101...
INFO: Finished greet_user in 0.00... seconds.
User 101 logged in.
Docstring: Fetches and greets a user.
Name: greet_user

Explanation: The decorators are applied from bottom to top. @log_execution is applied first to the original greet_user, then @repeat(2) is applied to the result of @log_execution. When greet_user(101) is called, the outer decorator (repeat) executes its logic, which includes calling the inner decorated function (log_execution wrapped greet_user) twice. Crucially, __doc__ and __name__ still refer to the original function's metadata.

Constraints

  • Your implementations should be written in Python 3.8+.
  • For timing in logging, you can use time.perf_counter() and approximate the duration. The exact timing is not critical, but the concept of measuring execution time is.
  • The logging output format should be clear and follow the examples. You can use the logging module or simple print statements prefixed with "INFO:" or your custom prefix.
  • Your code should handle functions that accept zero or more arguments.

Notes

  • Recall that decorators are essentially syntactic sugar for function calls. Think about how you would achieve the same result without the @ syntax.
  • The functools.wraps decorator is your friend for preserving function metadata. Make sure to use it!
  • Consider the order of execution when chaining decorators.
  • For decorators that accept arguments, you will typically need an outer function that takes the decorator's arguments and returns the actual decorator function.
Loading editor...
python