YouTip LogoYouTip

Python Decorator

Python Decorator Pattern

The core idea of the decorator pattern is wrapping β€” by creating a wrapper object (decorator) to wrap the original object, thereby extending its functionality without modifying the original object itself.

Basic Concepts

The decorator pattern consists of four main roles:

  1. Component Interface (Component): Defines the common interface for both the decorated object and the decorator.
  2. Concrete Component: The original object that needs to be decorated.
  3. Decorator Base Class (Decorator): Holds a reference to a component object and implements the component interface.
  4. Concrete Decorator: Implements specific decoration functionality.

Image 1


Basic Syntax of Decorators

Function Decorators

Function decorators are the most common form of decorators. They accept a function as an argument and return a new function.

Example

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Some operations before function execution")
        result = func(*args, **kwargs)
        print("Some operations after function execution")
        return result
    return wrapper

@my_decorator
def say_hello(name):
    print(f"Hello, {name}!")

# Using the decorator
say_hello("Alice")

Output:

Some operations before function execution
Hello, Alice!
Some operations after function execution

Decorators with Arguments

To pass arguments to a decorator, an additional layer of nesting is required:

Example

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(times):
                print(f"Execution {i+1}:")
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hello, {name}!")

greet("Bob")

Class Decorators

Besides function decorators, Python also supports class decorators. Class decorators work by implementing the __call__ method.

Basic Class Decorator

Example

class TimerDecorator:
    def __init__(self, func):
        self.func = func

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

@TimerDecorator
def calculate_sum(n):
    return sum(range(n))

result = calculate_sum(1000000)
print(f"Calculation result: {result}")

Class Decorator with Arguments

Example

class LogDecorator:
    def __init__(self, level="INFO"):
        self.level = level

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print(f"[{self.level}] Calling function: {func.__name__}")
            print(f"[{self.level}] Arguments: args={args}, kwargs={kwargs}")
            result = func(*args, **kwargs)
            print(f"[{self.level}] Return value: {result}")
            return result
        return wrapper

@LogDecorator(level="DEBUG")
def multiply(a, b):
    return a * b

multiply(5, 3)

Built-in Decorators

Python provides several useful built-in decorators:

@staticmethod and @classmethod

Example

class Calculator:
    @staticmethod
    def add(x, y):
        return x + y

    @classmethod
    def multiply(cls, x, y):
        return x * y

# Using static method
result1 = Calculator.add(5, 3)
print(f"Static method result: {result1}")

# Using class method
result2 = Calculator.multiply(5, 3)
print(f"Class method result: {result2}")

@property

Example

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Area: {circle.area}")

circle.radius = 10
print(f"New radius: {circle.radius}")
print(f"New area: {circle.area}")

Practical Applications of Decorators

1. Logging

Example

def log_execution(func):
    def wrapper(*args, **kwargs):
        print(f"Starting execution: {func.__name__}")
        try:
            result = func(*args, **kwargs)
            print(f"Successfully completed: {func.__name__}")
            return result
        except Exception as e:
            print(f"Execution failed: {func.__name__}, Error: {e}")
            raise
    return wrapper

@log_execution
def process_data(data):
    # Simulate data processing
    if not data:
        raise ValueError("Data cannot be empty")
    return [x * 2 for x in data]

# Test
data = [1, 2, 3]
result = process_data(data)
print(f"Processing result: {result}")

2. Permission Validation

Example

def require_login(func):
    def wrapper(user, *args, **kwargs):
        if not user.get('is_authenticated', False):
            raise PermissionError("User not logged in")
        return func(user, *args, **kwargs)
    return wrapper

@require_login
def view_profile(user):
    return f"Viewing profile for user {user['username']}"

# Test
user1 = {'username': 'alice', 'is_authenticated': True}
user2 = {'username': 'bob', 'is_authenticated': False}

print(view_profile(user1))  # Executes normally
# print(view_profile(user2))  # Raises PermissionError

3. Caching Decorator

Example

def cache_results(func):
    cache = {}
    def wrapper(*args):
        if args in cache:
            print(f"Retrieving result from cache: {args}")
            return cache
        result = func(*args)
        cache = result
        print(f"Computing and caching result: {args} -> {result}")
        return result
    return wrapper

@cache_results
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Test caching effect
print(fibonacci(5))
print(fibonacci(5))  # This time, result is retrieved from cache

Execution Order of Multiple Decorators

When multiple decorators are used, their execution order is from bottom to top:

Example

def decorator1(func):
    def wrapper():
        print("Decorator 1 - Before")
        func()
        print("Decorator 1 - After")
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2 - Before")
        func()
        print("Decorator 2 - After")
    return wrapper

@decorator1
@decorator2
def my_function():
    print("Original function")

my_function()

Output:

Decorator 1 - Before
Decorator 2 - Before
Original function
Decorator 2 - After
Decorator 1 - After


Preserving Function Metadata

When using decorators, the original function's metadata (e.g., function name, docstring) gets overwritten by the wrapper function. Use functools.wraps to preserve this metadata:

Example

from functools import wraps

def preserve_metadata(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Docstring of the wrapper function"""
        return func(*args, **kwargs)
    return wrapper

@preserve_metadata
def example_function():
    """This is the docstring of the original function"""
    pass

print(f"Function name: {example_function.__name__}")
print(f"Docstring: {example_function.__doc__}")

Practice Exercises

Exercise 1: Create a Performance Monitoring Decorator

Write a decorator to monitor function execution time and memory usage.

Example

import time
import tracemalloc
from functools import wraps

def performance_monitor(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Start memory tracking
        tracemalloc.start()
        # Record start time
        start_time = time.time()
        # Execute function
        result = func(*args, **kwargs)
        # Record end time
        end_time = time.time()
        # Get memory usage
        current, peak = tracemalloc.get_traced_memory()
        tracemalloc.stop()

        print(f"Performance report for function {func.__name__}:")
        print(f"Execution time: {end_time - start_time:.4f} seconds")
        print(f"Memory usage: Current {current/1024:.2f} KB, Peak {peak/1024:.2f} KB")
        return result
    return wrapper

@performance_monitor
def process_large_data():
    """Simulate processing large data"""
    data = [i**2 for i in range(100000)]
    return sum(data)

process_large_data()

Exercise 2: Implement a Retry Mechanism Decorator

Create a decorator that automatically retries a function a specified number of times upon failure.

Example

import time
from functools import wraps

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        print(f"Function {func.__name__} failed after maximum retry attempts")
                        raise
                    print(f"Function {func.__name__} failed on attempt {attempts}: {e}")
                    print(f"Retrying after {delay} seconds...")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def unstable_operation():
    """Simulate an unstable operation"""
    import random
    if random.random() < 0.7:  # 70% chance of failure
        raise ValueError("Random failure")
    return "Operation successful"

# Test retry mechanism
result = unstable_operation()
print(f"Final result: {result}")

Summary

Python decorators are a powerful and flexible tool, offering the following advantages:

  1. Code Reusability: Separates cross-cutting concerns (e.g., logging, caching, validation) from business logic.
  2. Dynamic Extension: Adds new functionality without modifying original code.
  3. Conciseness: Uses @ syntax for clearer, more readable code.
  4. Open/Closed Principle: Open for extension, closed for modification.

Best Practices for Using Decorators:

  • Use functools.wraps to preserve function metadata.
  • Maintain single responsibility for each decorator.
  • Handle exceptions appropriately within decorators.
  • Be mindful of decorator execution order.
← Python FacadePython Prototype β†’