πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
Search courses…
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Decorators & Metaclasses in Python

Python InterviewAdvanced Python⭐ Premium

Advertisement

Decorators & Metaclasses in Python

Difficulty: Hard | Companies: Google, Meta, Amazon, Netflix, Stripe

Function Decorators

Basic Decorator Patterns

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

def timer_decorator(func: Callable) -> Callable:
    """Measure execution time of a function."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        print(f"{func.__name__} executed in {end_time - start_time:.4f}s")
        return result
    return wrapper

def retry(max_attempts: int = 3, delay: float = 1.0):
    """Decorator with parameters for retrying failed operations."""
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            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:
                        time.sleep(delay * (2 ** attempt))  # Exponential backoff
            
            raise last_exception
        return wrapper
    return decorator

# Usage
@timer_decorator
def slow_function():
    time.sleep(0.1)
    return "Done"

@retry(max_attempts=5, delay=0.5)
def unreliable_api_call():
    import random
    if random.random() < 0.7:
        raise ConnectionError("API unavailable")
    return {"status": "success"}

ℹ️

Always use @wraps(func) to preserve the original function's metadata (name, docstring, etc.). This is crucial for debugging and introspection.

Stacking Decorators

def log_args(func):
    """Log function arguments."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        args_str = ", ".join([repr(a) for a in args])
        kwargs_str = ", ".join([f"{k}={v!r}" for k, v in kwargs.items()])
        print(f"Calling {func.__name__}({args_str}, {kwargs_str})")
        return func(*args, **kwargs)
    return wrapper

def cache_result(func):
    """Cache function results."""
    cache = {}
    
    @wraps(func)
    def wrapper(*args, **kwargs):
        key = (args, tuple(sorted(kwargs.items())))
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]
    
    wrapper.cache = cache
    return wrapper

def validate_positive(func):
    """Validate that all numeric arguments are positive."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        for arg in args:
            if isinstance(arg, (int, float)) and arg < 0:
                raise ValueError(f"Negative value not allowed: {arg}")
        for value in kwargs.values():
            if isinstance(value, (int, float)) and value < 0:
                raise ValueError(f"Negative value not allowed: {value}")
        return func(*args, **kwargs)
    return wrapper

# Stacked decorators - executed bottom-up
@log_args
@cache_result
@validate_positive
def calculate_discount(price: float, discount: float) -> float:
    """Calculate discounted price."""
    return price * (1 - discount / 100)

# All three decorators are applied
result = calculate_discount(100, 20)  # Logs, validates, caches, calculates

Class-Based Decorators

class CountCalls:
    """Decorator class that counts function calls."""
    
    def __init__(self, func):
        self.func = func
        self.call_count = 0
        self.call_history = []
    
    def __call__(self, *args, **kwargs):
        self.call_count += 1
        self.call_history.append({
            'args': args,
            'kwargs': kwargs,
            'timestamp': time.time()
        })
        return self.func(*args, **kwargs)
    
    def get_stats(self):
        return {
            'total_calls': self.call_count,
            'history': self.call_history
        }
    
    def reset(self):
        self.call_count = 0
        self.call_history = []

@CountCalls
def my_function(x, y):
    return x + y

# Usage
my_function(1, 2)
my_function(3, 4)
print(my_function.get_stats())  # {'total_calls': 2, 'history': [...]}

class RateLimiter:
    """Rate limiter decorator class."""
    
    def __init__(self, max_calls: int, period: float):
        self.max_calls = max_calls
        self.period = period
        self.calls = []
    
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            
            # Remove old calls outside the period
            self.calls = [call for call in self.calls if now - call < self.period]
            
            if len(self.calls) >= self.max_calls:
                raise RuntimeError(f"Rate limit exceeded: {self.max_calls} calls per {self.period}s")
            
            self.calls.append(now)
            return func(*args, **kwargs)
        
        wrapper.limiter = self
        return wrapper

@RateLimiter(max_calls=10, period=1.0)
def api_endpoint():
    return "Response"

Decorator Libraries

Implementation of a Decorator Factory

from typing import TypeVar, Callable, Any, Optional
from functools import wraps
import logging

F = TypeVar('F', bound=Callable[..., Any])

def log_execution(
    level: str = "INFO",
    include_args: bool = True,
    include_result: bool = False,
    logger_name: Optional[str] = None
) -> Callable[[F], F]:
    """
    Comprehensive logging decorator factory.
    
    Args:
        level: Logging level (DEBUG, INFO, WARNING, ERROR)
        include_args: Whether to log function arguments
        include_result: Whether to log return value
        logger_name: Name of the logger to use
    """
    def decorator(func: F) -> F:
        logger = logging.getLogger(logger_name or func.__module__)
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            log_func = getattr(logger, level.lower())
            
            # Log entry
            message = f"Entering {func.__qualname__}"
            if include_args:
                args_repr = [repr(a) for a in args]
                kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
                params = ", ".join(args_repr + kwargs_repr)
                message += f" with args: {params}"
            
            log_func(message)
            
            try:
                result = func(*args, **kwargs)
                
                # Log result
                if include_result:
                    log_func(f"Exiting {func.__qualname__} with result: {result!r}")
                else:
                    log_func(f"Exiting {func.__qualname__} successfully")
                
                return result
            
            except Exception as e:
                logger.error(f"Exception in {func.__qualname__}: {e}")
                raise
        
        return wrapper
    return decorator

# Usage
@log_execution(level="DEBUG", include_args=True, include_result=True)
def add_numbers(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

⚠️

Decorator factories with many parameters can become complex. Consider using dataclasses or configuration objects for decorator parameters.

Metaclasses

Understanding Metaclasses

class SingletonMeta(type):
    """Metaclass that implements Singleton pattern."""
    
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

class Database(metaclass=SingletonMeta):
    """Singleton database connection."""
    
    def __init__(self):
        self.connection = None
    
    def connect(self):
        print("Connecting to database...")
        self.connection = "connected"

# Usage - both variables point to same instance
db1 = Database()
db2 = Database()
print(db1 is db2)  # True

class ValidatedModelMeta(type):
    """Metaclass that adds validation to model classes."""
    
    def __new__(mcs, name, bases, namespace):
        # Collect validators from class definition
        validators = {}
        for key, value in namespace.items():
            if hasattr(value, '__validator__'):
                validators[key] = value
        
        namespace['_validators'] = validators
        
        # Create class
        cls = super().__new__(mcs, name, bases, namespace)
        
        # Add validation method
        def validate_instance(self):
            for field_name, validator in self._validators.items():
                value = getattr(self, field_name, None)
                if not validator(value):
                    raise ValueError(f"Validation failed for {field_name}")
        
        cls.validate = validate_instance
        return cls

def validate_type(expected_type):
    """Decorator that marks a field with its expected type."""
    def decorator(func):
        func.__validator__ = lambda x: isinstance(x, expected_type)
        return func
    return decorator

class User(metaclass=ValidatedModelMeta):
    name = validate_type(str)(lambda x: len(x) > 0)
    age = validate_type(int)(lambda x: 0 < x < 150)
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

# Usage
user = User("John", 30)
user.validate()  # Passes validation

Abstract Base Classes with Metaclasses

from abc import ABCMeta, abstractmethod
from typing import ClassVar

class PluginMeta(ABCMeta):
    """Metaclass for plugin system with auto-registration."""
    
    _plugins = {}
    
    def __init_subclass__(cls, plugin_name=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if plugin_name:
            PluginMeta._plugins[plugin_name] = cls
    
    @classmethod
    def get_plugin(mcs, name):
        return mcs._plugins.get(name)
    
    @classmethod
    def list_plugins(mcs):
        return list(mcs._plugins.keys())

class Plugin(metaclass=PluginMeta):
    """Base class for all plugins."""
    
    @abstractmethod
    def execute(self, data):
        pass
    
    @abstractmethod
    def get_name(self) -> str:
        pass

# Plugin implementations
class JsonPlugin(Plugin, plugin_name="json"):
    def execute(self, data):
        import json
        return json.dumps(data)
    
    def get_name(self):
        return "JSON Serializer"

class XmlPlugin(Plugin, plugin_name="xml"):
    def execute(self, data):
        return f"<data>{data}</data>"
    
    def get_name(self):
        return "XML Serializer"

# Usage
plugin = PluginMeta.get_plugin("json")()
print(plugin.execute({"key": "value"}))
print(PluginMeta.list_plugins())  # ['json', 'xml']

Advanced Decorator Patterns

Memoization Decorator with LRU Cache

from functools import wraps
from collections import OrderedDict
from typing import Any, Callable, Optional

class LRUCache:
    """Least Recently Used cache decorator."""
    
    def __init__(self, maxsize: int = 128, typed: bool = False):
        self.maxsize = maxsize
        self.typed = typed
        self.cache = OrderedDict()
        self.hits = 0
        self.misses = 0
    
    def __call__(self, func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Create cache key
            key = args
            if self.typed:
                key = key + tuple(type(v).__name__ for v in kwargs.values())
            key += tuple(sorted(kwargs.items()))
            
            # Check cache
            if key in self.cache:
                self.hits += 1
                self.cache.move_to_end(key)
                return self.cache[key]
            
            # Compute and cache
            self.misses += 1
            result = func(*args, **kwargs)
            self.cache[key] = result
            
            # Evict if necessary
            if len(self.cache) > self.maxsize:
                self.cache.popitem(last=False)
            
            return result
        
        wrapper.cache_info = lambda: {
            'hits': self.hits,
            'misses': self.misses,
            'size': len(self.cache),
            'maxsize': self.maxsize
        }
        wrapper.cache_clear = lambda: self.cache.clear()
        
        return wrapper

@LRUCache(maxsize=256)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Usage
print(fibonacci(50))
print(fibonacci.cache_info())

Property Decorators with Validation

class Property:
    """Descriptor-based property with validation."""
    
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        self.__doc__ = doc or (fget.__doc__ if fget else None)
    
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)
    
    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)
    
    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)
    
    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)
    
    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)
    
    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    @Property
    def celsius(self):
        """Temperature in Celsius."""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero")
        self._celsius = value
    
    @Property
    def fahrenheit(self):
        """Temperature in Fahrenheit."""
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9

# Usage
temp = Temperature(25)
print(temp.celsius)    # 25
print(temp.fahrenheit) # 77.0
temp.fahrenheit = 32
print(temp.celsius)    # 0.0

Follow-Up Questions

  1. Explain the difference between function decorators and class decorators.

  2. How do metaclasses differ from regular inheritance?

  3. When would you use a metaclass over a class decorator?

  4. How does the @property decorator work internally?

  5. Explain the concept of descriptor protocol and its relationship to decorators.

Advertisement