Function Power-Ups in Python: Mastering Decorators and Chaining

2024-04-11

Creating Function Decorators

Chaining Decorators

You can chain multiple decorators together by placing them one on top of the other before the function definition. When you do this, the decorators are applied in the order they are written, with the innermost decorator being executed first.

Example: Timing and Debugging Decorators

Here's a common example to illustrate decorators:

def timer(func):
  """Decorator that prints the execution time of a function."""
  import time

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

  return wrapper

def debug(func):
  """Decorator that prints the function name, arguments and return value."""
  def wrapper(*args, **kwargs):
    args_repr = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
    print(f"Calling '{func.__name__!r}' with {', '.join(args_repr)}")
    result = func(*args, **kwargs)
    print(f"Function '{func.__name__!r}' returned {result!r}")
    return result

  return wrapper

@debug  # Apply debug decorator first
@timer  # Apply timer decorator second
def factorial(n):
  """Calculates the factorial of a number."""
  if n == 0:
    return 1
  else:
    return n * factorial(n-1)

# Call the decorated function
factorial(5)

In this example, the debug decorator prints the function name, arguments, and return value. The timer decorator calculates and prints the execution time of the decorated function. By chaining them together using the @ symbol, both functionalities are applied to the factorial function.

This is a basic example of how decorators work. You can create decorators for various purposes like authentication, logging, caching, etc., and chain them together to achieve complex function behavior.




def timer(func):
  """Decorator that prints the execution time of a function."""
  import time

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

  return wrapper

def debug(func):
  """Decorator that prints the function name, arguments and return value."""
  def wrapper(*args, **kwargs):
    args_repr = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
    print(f"Calling '{func.__name__!r}' with {', '.join(args_repr)}")
    result = func(*args, **kwargs)
    print(f"Function '{func.__name__!r}' returned {result!r}")
    return result

  return wrapper

# Chain decorators (debug first, then timer)
@debug  
@timer
def factorial(n):
  """Calculates the factorial of a number."""
  if n == 0:
    return 1
  else:
    return n * factorial(n-1)

# Call the decorated function
factorial(5)

Explanation:

  1. Calling the decorated function: When we call factorial(5), the execution goes through the chained decorators:

    • The debug decorator first prints information about calling factorial with n=5.
    • Then, the factorial function calculates the factorial and returns the result.
    • Finally, the debug decorator prints information about the function returning the result.
    • After that, the timer decorator calculates the execution time and prints it.

This code demonstrates how you can chain decorators to achieve multiple functionalities on a single function.




  1. Class-based Decorators:

    • Instead of a function, you can define a class that takes the original function as an argument in its constructor.
    • Inside the class, define methods for the desired behavior (like pre-processing or post-processing).
    • Override the __call__ method to handle function execution and potentially perform actions before or after.
    • Use the class instance as the decorator with the @ symbol.
  2. Metaclasses:

    • Metaclasses are used to customize the behavior of classes themselves.
    • You can define a metaclass that inspects methods within a class definition.
    • Based on the method name or other criteria, the metaclass can dynamically modify the method behavior.
    • This approach is less common and more complex compared to decorators.
  3. Higher-Order Functions (HOFs):

    • You can write a function that takes the original function and additional logic as arguments.
    • This function can then return a new function that encapsulates the desired behavior.
    • While not strictly a decorator, HOFs offer a more flexible approach for complex logic.
class Timer:
  def __init__(self, func):
    self.func = func

  def __call__(self, *args, **kwargs):
    import time
    start_time = time.perf_counter()
    result = self.func(*args, **kwargs)
    end_time = time.perf_counter()
    print(f"Function '{self.func.__name__!r}' took {end_time - start_time:.4f} seconds")
    return result

@Timer
def factorial(n):
  """Calculates the factorial of a number."""
  if n == 0:
    return 1
  else:
    return n * factorial(n-1)

factorial(5)

Remember, decorators are generally preferred due to their concise syntax and readability. These alternatives offer more control but might be less Pythonic for simple use cases.


python function decorator


Beyond the Basics: Exploring Advanced Attribute Handling in Python

Python provides the built-in function setattr to achieve this. It takes three arguments:object: The object you want to modify...


Enhancing Django Forms with CSS: A Guide to Customization

Understanding the Need for CSS Classes:Django forms generate HTML code for input elements like text fields, checkboxes, etc...


Pandas Tip: Limit the Number of Rows Shown When Printing DataFrames

In pandas, you can set the maximum number of rows shown when printing a DataFrame using the display. max_rows option. This is a formatting setting that affects how pandas presents your data...


Managing Database Connections: How to Close SQLAlchemy Sessions

SQLAlchemy Sessions and ClosingIn SQLAlchemy, a session represents a conversation with your database. It keeps track of any changes you make to objects loaded from the database...


Alternative Techniques for Handling Duplicate Rows in Pandas DataFrames

Concepts:Python: A general-purpose programming language widely used for data analysis and scientific computing.Pandas: A powerful Python library specifically designed for data manipulation and analysis...


python function decorator