Decorators - CameronAuler/python-devops GitHub Wiki

Table of Contents

Function Decorators

A function decorator is a function that wraps another function to modify its behavior. It is applied using the @decorator_name syntax before a function definition and it is used for logging, enforcing access control, performance measurement, and more.

Basic Function Decorator

  • my_decorator wraps my_function inside wrapper().
  • When my_function() is called, it actually calls wrapper() instead.
  • This allows adding pre-processing and post-processing logic.
def my_decorator(func):
    def wrapper():
        print("Before function execution")
        func()
        print("After function execution")
    return wrapper

@my_decorator  # Equivalent to my_function = my_decorator(my_function)
def my_function():
    print("Hello, World!")

my_function()
# Output:
Before function execution
Hello, World!
After function execution

Function Decorator with Arguments

If the decorated function takes arguments, the wrapper function must accept *args and **kwargs.

  • The decorator repeat_decorator calls the function twice.
  • *args, **kwargs allow it to handle any function arguments.
def repeat_decorator(func):
    def wrapper(*args, **kwargs):
        print("Function is being executed twice:")
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper

@repeat_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")
# Output:
Function is being executed twice:
Hello, Alice!
Hello, Alice!

Function Decorator with Parameters

To pass arguments to the decorator itself, wrap it inside another function.

  • The repeat(n) function returns a decorator that calls func() n times.
def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)  # Repeat function 3 times
def say_hello():
    print("Hello!")

say_hello()
# Output:
Hello!
Hello!
Hello!

Class Decorators

A class decorator is a class that wraps a function or another class.

Class as a Decorator

The __call__ method allows instances of the class to be used as decorators.

  • Logger class stores the function (self.func).
  • __call__ executes self.func and logs its call.
class Logger:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f"Calling function: {self.func.__name__}")
        return self.func(*args, **kwargs)

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

print(add(2, 3))
# Output:
Calling function: add
5

Using Class Decorators to Track Calls

  • The CallCounter decorator counts how many times a function is called.
class CallCounter:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Function {self.func.__name__} has been called {self.count} times.")
        return self.func(*args, **kwargs)

@CallCounter
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")
greet("Bob")
# Output:
Function greet has been called 1 times.
Hello, Alice!
Function greet has been called 2 times.
Hello, Bob!

functools.wraps

When a function is decorated, the original function’s metadata (name, docstring, etc.) is overwritten by the wrapper. functools.wraps can be used to fix the metadata.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function execution")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    """This function says hello."""
    print("Hello!")

print(say_hello.__name__)  # Outputs "wrapper" instead of "say_hello"
print(say_hello.__doc__)  # None
# Output:
wrapper
None

Fixing the Metadata with functools.wraps

functools.wraps preserves the original function’s metadata.

  • @wraps(func) tells Python to copy metadata from func to wrapper.
from functools import wraps

def my_decorator(func):
    @wraps(func)  # Preserves metadata
    def wrapper(*args, **kwargs):
        print("Before function execution")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    """This function says hello."""
    print("Hello!")

print(say_hello.__name__)  # Outputs "say_hello"
print(say_hello.__doc__)  # Outputs correct docstring
# Output:
say_hello
This function says hello.