Navigation

Python

Python Function Factories and Parameterized Decorators: Advanced Design Patterns

Master Python function factories and parameterized decorators! Learn advanced patterns for creating dynamic functions, configurable decorators, and reusable code components.

Table Of Contents

Introduction

Function factories and parameterized decorators are powerful Python patterns that allow you to create highly flexible, reusable, and configurable code. These advanced techniques enable you to generate functions dynamically and create decorators that can be customized with parameters, leading to more maintainable and DRY (Don't Repeat Yourself) code.

In this comprehensive guide, we'll explore function factories, parameterized decorators, and advanced patterns that combine both concepts to create sophisticated, reusable components for your Python applications.

Understanding Function Factories

Function factories are functions that create and return other functions. They're particularly useful when you need to generate similar functions with slight variations or when you want to encapsulate configuration within the created function.

Basic Function Factory Pattern

def create_multiplier(factor: int):
    """Factory function that creates multiplier functions."""
    def multiplier(value: int) -> int:
        return value * factor
    return multiplier

# Create specific multiplier functions
double = create_multiplier(2)
triple = create_multiplier(3)
ten_times = create_multiplier(10)

# Usage
print(double(5))     # Output: 10
print(triple(4))     # Output: 12
print(ten_times(3))  # Output: 30

# Each function remembers its factor (closure)
print(double.__closure__[0].cell_contents)  # Output: 2

Factory with Multiple Parameters

def create_formatter(prefix: str, suffix: str, separator: str = " "):
    """Factory that creates string formatting functions."""
    def formatter(*args) -> str:
        formatted_args = separator.join(str(arg) for arg in args)
        return f"{prefix}{formatted_args}{suffix}"
    return formatter

# Create different formatting functions
html_formatter = create_formatter("<span>", "</span>")
bracket_formatter = create_formatter("[", "]", ", ")
debug_formatter = create_formatter("DEBUG: ", " - END", " | ")

# Usage
print(html_formatter("Hello", "World"))
# Output: <span>Hello World</span>

print(bracket_formatter("apple", "banana", "cherry"))
# Output: [apple, banana, cherry]

print(debug_formatter("user", "login", "success"))
# Output: DEBUG: user | login | success - END

Configuration-Based Function Factories

from typing import Dict, Any, Callable
from dataclasses import dataclass

@dataclass
class ValidationConfig:
    min_length: int = 0
    max_length: int = 1000
    allowed_chars: str = ""
    required_patterns: list[str] = None
    forbidden_patterns: list[str] = None

def create_validator(config: ValidationConfig) -> Callable[[str], bool]:
    """Factory that creates validation functions based on configuration."""
    
    def validator(text: str) -> bool:
        # Length validation
        if len(text) < config.min_length or len(text) > config.max_length:
            return False
        
        # Character validation
        if config.allowed_chars:
            if not all(char in config.allowed_chars for char in text):
                return False
        
        # Required patterns
        if config.required_patterns:
            import re
            for pattern in config.required_patterns:
                if not re.search(pattern, text):
                    return False
        
        # Forbidden patterns
        if config.forbidden_patterns:
            import re
            for pattern in config.forbidden_patterns:
                if re.search(pattern, text):
                    return False
        
        return True
    
    return validator

# Create different validators
email_config = ValidationConfig(
    min_length=5,
    max_length=100,
    required_patterns=[r'^[^@]+@[^@]+\.[^@]+$']
)

password_config = ValidationConfig(
    min_length=8,
    max_length=50,
    required_patterns=[r'[A-Z]', r'[a-z]', r'\d', r'[!@#$%^&*]'],
    forbidden_patterns=[r'\s']  # No whitespace
)

username_config = ValidationConfig(
    min_length=3,
    max_length=20,
    allowed_chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
)

# Create validator functions
email_validator = create_validator(email_config)
password_validator = create_validator(password_config)
username_validator = create_validator(username_config)

# Usage
print(email_validator("user@example.com"))  # True
print(password_validator("SecurePass123!"))  # True
print(username_validator("valid_user123"))   # True

Parameterized Decorators

Parameterized decorators are decorators that accept arguments, allowing you to customize their behavior. They're essentially decorator factories that return actual decorators.

Basic Parameterized Decorator

import time
from functools import wraps
from typing import Callable, Any

def retry(max_attempts: int = 3, delay: float = 1.0):
    """Parameterized decorator for retrying failed function calls."""
    
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            last_exception = None
            
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < max_attempts - 1:
                        print(f"Attempt {attempt + 1} failed, retrying in {delay}s...")
                        time.sleep(delay)
                    else:
                        print(f"All {max_attempts} attempts failed")
            
            raise last_exception
        
        return wrapper
    return decorator

# Usage with different parameters
@retry(max_attempts=5, delay=0.5)
def unreliable_network_call():
    """Simulate an unreliable network operation."""
    import random
    if random.random() < 0.7:
        raise ConnectionError("Network timeout")
    return "Success!"

@retry(max_attempts=2, delay=2.0)
def another_unreliable_function():
    """Another function with different retry parameters."""
    import random
    if random.random() < 0.8:
        raise ValueError("Random error")
    return "Operation completed"

Advanced Parameterized Decorator with Multiple Options

import time
import logging
from functools import wraps
from typing import Callable, Any, Optional, Union

def monitor(
    log_entry: bool = True,
    log_exit: bool = True,
    log_duration: bool = True,
    log_args: bool = False,
    log_result: bool = False,
    logger: Optional[logging.Logger] = None,
    level: int = logging.INFO
):
    """Advanced monitoring decorator with multiple configuration options."""
    
    def decorator(func: Callable) -> Callable:
        # Use provided logger or create default
        _logger = logger or logging.getLogger(func.__module__)
        
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            func_name = func.__name__
            
            # Log function entry
            if log_entry:
                entry_msg = f"Entering {func_name}"
                if log_args:
                    entry_msg += f" with args={args}, kwargs={kwargs}"
                _logger.log(level, entry_msg)
            
            start_time = time.time()
            
            try:
                result = func(*args, **kwargs)
                
                # Log function exit
                if log_exit:
                    exit_msg = f"Exiting {func_name}"
                    if log_result:
                        exit_msg += f" with result={result}"
                    _logger.log(level, exit_msg)
                
                return result
                
            except Exception as e:
                _logger.error(f"Exception in {func_name}: {e}")
                raise
            
            finally:
                # Log duration
                if log_duration:
                    duration = time.time() - start_time
                    _logger.log(level, f"{func_name} took {duration:.4f} seconds")
        
        return wrapper
    return decorator

# Usage examples
@monitor(log_args=True, log_result=True, log_duration=True)
def calculate_fibonacci(n: int) -> int:
    """Calculate Fibonacci number."""
    if n <= 1:
        return n
    return calculate_fibonacci(n - 1) + calculate_fibonacci(n - 2)

@monitor(log_entry=True, log_exit=False, log_duration=True, level=logging.DEBUG)
def process_data(data: list[int]) -> int:
    """Process a list of integers."""
    return sum(x * x for x in data)

Conditional Parameterized Decorators

import os
from functools import wraps
from typing import Callable, Any

def cache_if(condition: bool = True, cache_size: int = 128):
    """Conditionally apply caching based on condition."""
    
    def decorator(func: Callable) -> Callable:
        if condition:
            from functools import lru_cache
            return lru_cache(maxsize=cache_size)(func)
        else:
            return func
    
    return decorator

def debug_if(debug_mode: bool = None):
    """Add debugging only if debug mode is enabled."""
    
    if debug_mode is None:
        debug_mode = os.getenv("DEBUG", "false").lower() == "true"
    
    def decorator(func: Callable) -> Callable:
        if debug_mode:
            @wraps(func)
            def wrapper(*args: Any, **kwargs: Any) -> Any:
                print(f"DEBUG: Calling {func.__name__} with args={args}, kwargs={kwargs}")
                result = func(*args, **kwargs)
                print(f"DEBUG: {func.__name__} returned {result}")
                return result
            return wrapper
        else:
            return func
    
    return decorator

# Usage
@cache_if(condition=True, cache_size=64)
@debug_if(debug_mode=True)
def expensive_calculation(x: int, y: int) -> int:
    """Perform an expensive calculation."""
    import time
    time.sleep(0.1)  # Simulate expensive operation
    return x ** y + y ** x

# Test the function
result = expensive_calculation(2, 3)
print(f"Result: {result}")

Combining Function Factories and Parameterized Decorators

Decorator Factory Pattern

from typing import Callable, Any, Dict
from functools import wraps
import json

def create_api_decorator(
    base_url: str,
    default_headers: Dict[str, str] = None,
    timeout: int = 30,
    auth_required: bool = True
):
    """Factory that creates API-specific decorators."""
    
    if default_headers is None:
        default_headers = {"Content-Type": "application/json"}
    
    def api_endpoint(path: str, method: str = "GET"):
        """Parameterized decorator for API endpoints."""
        
        def decorator(func: Callable) -> Callable:
            @wraps(func)
            def wrapper(*args: Any, **kwargs: Any) -> Any:
                # Simulate API call logic
                full_url = f"{base_url.rstrip('/')}/{path.lstrip('/')}"
                
                print(f"Making {method} request to {full_url}")
                print(f"Headers: {default_headers}")
                print(f"Timeout: {timeout}s")
                print(f"Auth required: {auth_required}")
                
                # Call the original function (would contain actual logic)
                result = func(*args, **kwargs)
                
                # Simulate response processing
                return {
                    "url": full_url,
                    "method": method,
                    "data": result,
                    "status": "success"
                }
            
            return wrapper
        return decorator
    
    return api_endpoint

# Create specific API decorators
github_api = create_api_decorator(
    base_url="https://api.github.com",
    default_headers={"Accept": "application/vnd.github.v3+json"},
    auth_required=True
)

internal_api = create_api_decorator(
    base_url="https://internal.company.com/api",
    default_headers={"Content-Type": "application/json", "X-API-Version": "v1"},
    timeout=60,
    auth_required=False
)

# Usage
@github_api("/users/{username}", "GET")
def get_github_user(username: str):
    """Get GitHub user information."""
    return {"username": username, "source": "github"}

@internal_api("/metrics", "POST")
def submit_metrics(metrics: dict):
    """Submit metrics to internal API."""
    return {"metrics": metrics, "submitted": True}

# Test the functions
user_data = get_github_user("octocat")
metrics_result = submit_metrics({"cpu": 80, "memory": 65})

Dynamic Function Generator

from typing import Dict, Any, Callable, List
import operator

def create_data_processor(operations: List[str], 
                         filters: Dict[str, Any] = None,
                         transformations: Dict[str, Callable] = None):
    """Factory that creates data processing functions based on configuration."""
    
    if filters is None:
        filters = {}
    
    if transformations is None:
        transformations = {}
    
    # Available operations
    available_ops = {
        'sum': sum,
        'max': max,
        'min': min,
        'count': len,
        'avg': lambda x: sum(x) / len(x) if x else 0
    }
    
    def process_data(data: List[Any]) -> Dict[str, Any]:
        """Generated data processing function."""
        
        # Apply filters
        filtered_data = data
        for filter_key, filter_value in filters.items():
            if filter_key == 'min_value':
                filtered_data = [x for x in filtered_data if x >= filter_value]
            elif filter_key == 'max_value':
                filtered_data = [x for x in filtered_data if x <= filter_value]
            elif filter_key == 'type':
                filtered_data = [x for x in filtered_data if isinstance(x, filter_value)]
        
        # Apply transformations
        transformed_data = filtered_data
        for transform_name, transform_func in transformations.items():
            transformed_data = [transform_func(x) for x in transformed_data]
        
        # Apply operations
        results = {}
        for op in operations:
            if op in available_ops and transformed_data:
                try:
                    results[op] = available_ops[op](transformed_data)
                except (TypeError, ValueError) as e:
                    results[op] = f"Error: {e}"
        
        return {
            'original_count': len(data),
            'filtered_count': len(filtered_data),
            'processed_count': len(transformed_data),
            'results': results
        }
    
    return process_data

# Create different processors
numeric_processor = create_data_processor(
    operations=['sum', 'avg', 'max', 'min'],
    filters={'type': (int, float), 'min_value': 0},
    transformations={'square': lambda x: x ** 2}
)

string_processor = create_data_processor(
    operations=['count'],
    filters={'type': str},
    transformations={'length': len, 'upper': str.upper}
)

# Usage
numbers = [1, 2, 3, -1, 4.5, "not a number", 0, 10]
strings = ["hello", "world", 123, "python", "factory"]

numeric_result = numeric_processor(numbers)
string_result = string_processor(strings)

print("Numeric processing:", numeric_result)
print("String processing:", string_result)

Advanced Patterns and Use Cases

Registry Pattern with Function Factories

from typing import Dict, Callable, Any, TypeVar, Type
from abc import ABC, abstractmethod

T = TypeVar('T')

class HandlerRegistry:
    """Registry for dynamically created handlers."""
    
    def __init__(self):
        self._handlers: Dict[str, Callable] = {}
        self._factories: Dict[str, Callable] = {}
    
    def register_factory(self, name: str, factory: Callable):
        """Register a factory function."""
        self._factories[name] = factory
    
    def create_handler(self, handler_type: str, **config) -> Callable:
        """Create a handler using registered factory."""
        if handler_type not in self._factories:
            raise ValueError(f"Unknown handler type: {handler_type}")
        
        factory = self._factories[handler_type]
        handler = factory(**config)
        return handler
    
    def register_handler(self, name: str, handler: Callable):
        """Register a pre-created handler."""
        self._handlers[name] = handler
    
    def get_handler(self, name: str) -> Callable:
        """Get a registered handler."""
        if name not in self._handlers:
            raise ValueError(f"Unknown handler: {name}")
        return self._handlers[name]
    
    def list_handlers(self) -> List[str]:
        """List all registered handlers."""
        return list(self._handlers.keys())

# Factory functions for different handler types
def create_file_handler(filename: str, mode: str = 'a'):
    """Factory for file-based handlers."""
    def handler(message: str) -> None:
        with open(filename, mode) as f:
            f.write(f"{message}\n")
    return handler

def create_email_handler(smtp_server: str, sender: str, recipients: List[str]):
    """Factory for email handlers."""
    def handler(message: str) -> None:
        print(f"Sending email via {smtp_server}")
        print(f"From: {sender}")
        print(f"To: {recipients}")
        print(f"Message: {message}")
    return handler

def create_webhook_handler(url: str, headers: Dict[str, str] = None):
    """Factory for webhook handlers."""
    if headers is None:
        headers = {}
    
    def handler(message: str) -> None:
        print(f"Sending webhook to {url}")
        print(f"Headers: {headers}")
        print(f"Payload: {message}")
    return handler

# Usage
registry = HandlerRegistry()

# Register factories
registry.register_factory("file", create_file_handler)
registry.register_factory("email", create_email_handler)
registry.register_factory("webhook", create_webhook_handler)

# Create and register handlers
log_handler = registry.create_handler("file", filename="app.log", mode="a")
alert_handler = registry.create_handler(
    "email", 
    smtp_server="smtp.company.com",
    sender="alerts@company.com",
    recipients=["admin@company.com"]
)
webhook_handler = registry.create_handler(
    "webhook",
    url="https://hooks.company.com/alerts",
    headers={"Authorization": "Bearer token123"}
)

registry.register_handler("logging", log_handler)
registry.register_handler("alerts", alert_handler)
registry.register_handler("webhook", webhook_handler)

# Use handlers
for handler_name in registry.list_handlers():
    handler = registry.get_handler(handler_name)
    handler(f"Test message from {handler_name}")

Context-Aware Function Factories

import threading
from typing import Any, Dict, Optional
from contextlib import contextmanager

class ContextualFactory:
    """Factory that creates functions aware of execution context."""
    
    def __init__(self):
        self._context = threading.local()
    
    @contextmanager
    def context(self, **context_vars):
        """Context manager for setting execution context."""
        old_context = getattr(self._context, 'vars', {})
        self._context.vars = {**old_context, **context_vars}
        try:
            yield
        finally:
            self._context.vars = old_context
    
    def get_context(self, key: str, default: Any = None) -> Any:
        """Get a value from current context."""
        return getattr(self._context, 'vars', {}).get(key, default)
    
    def create_logger(self, name: str):
        """Create a context-aware logger function."""
        def log(level: str, message: str) -> None:
            user = self.get_context('user', 'anonymous')
            request_id = self.get_context('request_id', 'no-request')
            environment = self.get_context('environment', 'unknown')
            
            formatted_message = (
                f"[{environment}] [{level.upper()}] "
                f"[User: {user}] [Request: {request_id}] "
                f"{name}: {message}"
            )
            print(formatted_message)
        
        return log
    
    def create_cache_key_generator(self, prefix: str):
        """Create a context-aware cache key generator."""
        def generate_key(*args, **kwargs) -> str:
            tenant = self.get_context('tenant', 'default')
            version = self.get_context('api_version', 'v1')
            
            key_parts = [prefix, tenant, version]
            key_parts.extend(str(arg) for arg in args)
            key_parts.extend(f"{k}={v}" for k, v in sorted(kwargs.items()))
            
            return ":".join(key_parts)
        
        return generate_key

# Usage
factory = ContextualFactory()

# Create context-aware functions
logger = factory.create_logger("UserService")
cache_key_gen = factory.create_cache_key_generator("user_data")

# Use functions in different contexts
with factory.context(user="alice", request_id="req-123", environment="prod"):
    logger("info", "User logged in")
    key = cache_key_gen("profile", user_id=123)
    print(f"Cache key: {key}")

with factory.context(user="bob", request_id="req-456", environment="dev", tenant="acme"):
    logger("warning", "Invalid password attempt")
    key = cache_key_gen("settings", user_id=456, theme="dark")
    print(f"Cache key: {key}")

Testing Function Factories and Parameterized Decorators

import pytest
from unittest.mock import patch, MagicMock

def test_function_factory():
    """Test basic function factory functionality."""
    # Test the multiplier factory from earlier
    multiplier_factory = create_multiplier
    
    double = multiplier_factory(2)
    triple = multiplier_factory(3)
    
    assert double(5) == 10
    assert triple(4) == 12
    assert double(0) == 0
    
    # Test that each function maintains its own closure
    assert double(1) != triple(1)

def test_parameterized_decorator():
    """Test parameterized decorator functionality."""
    
    # Create a mock function that fails twice then succeeds
    call_count = 0
    def failing_function():
        nonlocal call_count
        call_count += 1
        if call_count < 3:
            raise Exception("Test failure")
        return "success"
    
    # Apply retry decorator
    decorated_function = retry(max_attempts=3, delay=0.01)(failing_function)
    
    # Test that it succeeds after retries
    result = decorated_function()
    assert result == "success"
    assert call_count == 3

def test_registry_pattern():
    """Test the registry pattern with function factories."""
    registry = HandlerRegistry()
    
    # Test factory registration
    registry.register_factory("test", lambda msg="hello": lambda: msg)
    
    # Test handler creation
    handler = registry.create_handler("test", msg="world")
    assert handler() == "world"
    
    # Test handler registration
    registry.register_handler("test_handler", handler)
    retrieved_handler = registry.get_handler("test_handler")
    assert retrieved_handler() == "world"
    
    # Test error cases
    with pytest.raises(ValueError):
        registry.create_handler("nonexistent")
    
    with pytest.raises(ValueError):
        registry.get_handler("nonexistent")

@patch('time.sleep')  # Mock sleep to speed up tests
def test_retry_decorator_failure(mock_sleep):
    """Test retry decorator when all attempts fail."""
    
    def always_failing_function():
        raise ValueError("Always fails")
    
    decorated_function = retry(max_attempts=2, delay=0.1)(always_failing_function)
    
    with pytest.raises(ValueError, match="Always fails"):
        decorated_function()
    
    # Verify sleep was called once (between attempts)
    mock_sleep.assert_called_once_with(0.1)

FAQ

Q: When should I use function factories instead of classes? A: Use function factories when you need simple, stateless behavior generators. Use classes when you need complex state management, inheritance, or multiple methods.

Q: How do parameterized decorators differ from regular decorators? A: Parameterized decorators accept arguments that customize their behavior, while regular decorators have fixed behavior. Parameterized decorators are actually decorator factories.

Q: Can I stack multiple parameterized decorators? A: Yes, you can stack them just like regular decorators. Be aware of the order - decorators are applied from bottom to top.

Q: How do I handle type hints with function factories? A: Use typing.Callable and generic types. For complex cases, consider using typing.Protocol to define the interface of generated functions.

Q: Are there performance implications? A: Function factories and parameterized decorators add some overhead, but it's usually negligible. The benefits in code organization and reusability typically outweigh the minor performance cost.

Q: How do I debug issues with generated functions? A: Use descriptive names, add logging, and preserve function metadata with functools.wraps. Consider adding debug modes to your factories.

Conclusion

Function factories and parameterized decorators are powerful patterns that enable:

  1. Dynamic function generation based on configuration
  2. Highly configurable decorators for cross-cutting concerns
  3. Reusable code components that reduce duplication
  4. Flexible APIs that adapt to different use cases
  5. Separation of concerns between configuration and implementation

Key best practices include:

  • Use clear, descriptive names for generated functions
  • Document the behavior and parameters thoroughly
  • Handle edge cases and validation in factories
  • Preserve function metadata with functools.wraps
  • Test both the factories and their generated functions
  • Consider performance implications for frequently-called code

These patterns are particularly valuable in frameworks, libraries, and applications that need high configurability and code reuse. Master them to write more elegant, maintainable Python code that adapts to changing requirements without duplication.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Python