Table Of Contents
- Introduction
- Understanding Function Factories
- Parameterized Decorators
- Combining Function Factories and Parameterized Decorators
- Advanced Patterns and Use Cases
- Testing Function Factories and Parameterized Decorators
- FAQ
- Conclusion
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:
- Dynamic function generation based on configuration
- Highly configurable decorators for cross-cutting concerns
- Reusable code components that reduce duplication
- Flexible APIs that adapt to different use cases
- 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.
Add Comment
No comments yet. Be the first to comment!