🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
Search courses…
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

Decorators: Function, Class, Parameterized, Stacking

PythonDecorators & Metaprogramming⭐ Premium

Advertisement

Google, Meta & Amazon Interview

Decorators: Function, Class, Parameterized, Stacking

Advanced decorator patterns and metaprogramming techniques

Interview Question

"Explain Python decorators in depth. What's the difference between function and class decorators? How do parameterized decorators work? Show advanced patterns like stacking, caching, and async decorators."

Difficulty: Hard | Frequently asked at Google, Meta, Amazon, Microsoft


Theoretical Foundation

What is a Decorator?

A decorator is a function that takes another function as input and returns a new function that usually extends the behavior of the original function.

# Basic decorator syntax
@decorator
def function():
    pass

# Equivalent without syntactic sugar
def function():
    pass
function = decorator(function)

ℹ️

Key Concept: Decorators are just functions that take functions and return functions. The @ syntax is syntactic sugar.

Function Decorators

import functools
import time
from typing import Callable, Any

# Basic decorator
def simple_decorator(func):
    """Simple decorator example."""
    def wrapper(*args, **kwargs):
        print(f"Before {func.__name__}")
        result = func(*args, **kwargs)
        print(f"After {func.__name__}")
        return result
    return wrapper

@simple_decorator
def greet(name):
    return f"Hello, {name}!"

print(greet("World"))
# Output:
# Before greet
# After greet
# Hello, World!

Preserving Function Metadata

import functools

def proper_decorator(func):
    """Decorator that preserves function metadata."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """Wrapper function documentation."""
        return func(*args, **kwargs)
    return wrapper

@proper_decorator
def documented_function():
    """This is the actual function documentation."""
    pass

# Without functools.wraps
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def bad_documented():
    """This documentation is lost."""
    pass

print(f"With wraps: {documented_function.__name__}, {documented_function.__doc__}")
print(f"Without wraps: {bad_documented.__name__}, {bad_documented.__doc__}")

Output:

Architecture Diagram
With wraps: documented_function, This is the actual function documentation.
Without wraps: wrapper, None

Parameterized Decorators

Decorators with Arguments

import functools
import time
from typing import Callable, Optional

# Parameterized decorator
def repeat(times: int):
    """Repeat function execution."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def say_hello():
    print("Hello!")

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

# Parameterized decorator with default values
def retry(max_attempts: int = 3, delay: float = 1.0):
    """Retry function on failure."""
    def decorator(func):
        @functools.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)
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.1)
def unreliable_function():
    """Function that might fail."""
    import random
    if random.random() < 0.5:
        raise ValueError("Random failure")
    return "Success!"

# Multiple parameters
def rate_limit(calls_per_second: float):
    """Rate limit function calls."""
    min_interval = 1.0 / calls_per_second
    last_called = [0.0]
    
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[0]
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            last_called[0] = time.time()
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limit(calls_per_second=10)
def api_call():
    print("API call executed")

💡

Interview Tip: Parameterized decorators use three levels of nested functions: outer (parameters), middle (decorator), inner (wrapper).


Class Decorators

Using Classes as Decorators

import functools
import time
from typing import Callable, Any

# Class decorator implementing __call__
class CountCalls:
    """Count function calls using a class."""
    
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0
    
    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call #{self.num_calls} to {self.func.__name__}")
        return self.func(*args, **kwargs)
    
    def reset(self):
        self.num_calls = 0

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

result = fibonacci(5)
print(f"Result: {result}")
print(f"Total calls: {fibonacci.num_calls}")

Output:

Architecture Diagram
Call #1 to fibonacci
Call #2 to fibonacci
Call #3 to fibonacci
Call #4 to fibonacci
Call #5 to fibonacci
Call #6 to fibonacci
Call #7 to fibonacci
Call #8 to fibonacci
Call #9 to fibonacci
Result: 5
Total calls: 9

Stateful Class Decorators

import functools
import time
from typing import Callable, Any, Dict, List

class Cache:
    """LRU Cache implementation using class decorator."""
    
    def __init__(self, maxsize: int = 128):
        self.maxsize = maxsize
        self.cache: Dict[Any, Any] = {}
        self.hits = 0
        self.misses = 0
    
    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            key = str(args) + str(kwargs)
            
            if key in self.cache:
                self.hits += 1
                return self.cache[key]
            
            self.misses += 1
            result = func(*args, **kwargs)
            
            if len(self.cache) >= self.maxsize:
                # Remove oldest item
                oldest_key = next(iter(self.cache))
                del self.cache[oldest_key]
            
            self.cache[key] = result
            return result
        
        wrapper.cache_info = self.cache_info
        wrapper.cache_clear = self.cache_clear
        return wrapper
    
    def cache_info(self):
        return {
            'hits': self.hits,
            'misses': self.misses,
            'size': len(self.cache)
        }
    
    def cache_clear(self):
        self.cache.clear()
        self.hits = 0
        self.misses = 0

@Cache(maxsize=100)
def expensive_computation(n):
    """Expensive function with caching."""
    time.sleep(0.01)  # Simulate work
    return n * n

# Usage
print(expensive_computation(5))
print(expensive_computation(5))  # Cache hit
print(expensive_computation.cache_info())

ℹ️

Class vs Function Decorators: Class decorators are better for stateful decorators that need to maintain internal state.


Stacking Decorators

Multiple Decorators

import functools
import time
from typing import Callable

# Multiple decorators
def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

def underline(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"<u>{func(*args, **kwargs)}</u>"
    return wrapper

@bold
@italic
@underline
def greet(name):
    return f"Hello, {name}!"

print(greet("World"))
# Output: <b><i><u>Hello, World!</u></i></b>

# Execution order: bold(italic(underline(greet)))
# greet() -> underline() -> italic() -> bold()

Decorator Ordering

import functools

def decorator_a(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Decorator A - Before")
        result = func(*args, **kwargs)
        print("Decorator A - After")
        return result
    return wrapper

def decorator_b(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Decorator B - Before")
        result = func(*args, **kwargs)
        print("Decorator B - After")
        return result
    return wrapper

@decorator_a
@decorator_b
def my_function():
    print("Function executed")

my_function()
# Output:
# Decorator A - Before
# Decorator B - Before
# Function executed
# Decorator B - After
# Decorator A - After

# Order matters! @decorator_a @decorator_b means:
# my_function = decorator_a(decorator_b(my_function))

⚠️

Common Mistake: Decorators apply bottom-up but execute top-down. Understanding this ordering is crucial for debugging.


Advanced Decorator Patterns

Decorator Factory

import functools
from typing import Callable, Optional

def log(level: str = "INFO", prefix: Optional[str] = None):
    """Decorator factory for logging."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            prefix_str = prefix or func.__name__
            print(f"[{level}] {prefix_str}: Called with {args}, {kwargs}")
            result = func(*args, **kwargs)
            print(f"[{level}] {prefix_str}: Returned {result}")
            return result
        return wrapper
    return decorator

@log(level="DEBUG", prefix="UserService")
def get_user(user_id: int):
    return {"id": user_id, "name": "John"}

user = get_user(123)

Output:

Architecture Diagram
[DEBUG] UserService: Called with (123,), {}
[DEBUG] UserService: Returned {'id': 123, 'name': 'John'}

Decorator with Access Control

import functools
from typing import Callable, Any

class PermissionError(Exception):
    pass

def require_permission(permission: str):
    """Decorator for access control."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Get user from context (simplified)
            user = kwargs.get('user') or (args[0] if args else None)
            
            if not user:
                raise PermissionError("No user provided")
            
            if permission not in user.get('permissions', []):
                raise PermissionError(f"Missing permission: {permission}")
            
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_permission("admin")
def delete_user(user_id: int, user: dict):
    return f"Deleted user {user_id}"

# Usage
admin_user = {"name": "Admin", "permissions": ["admin", "write"]}
regular_user = {"name": "User", "permissions": ["read"]}

try:
    delete_user(123, user=admin_user)
    print("Success!")
except PermissionError as e:
    print(f"Error: {e}")

try:
    delete_user(123, user=regular_user)
except PermissionError as e:
    print(f"Error: {e}")

Output:

Architecture Diagram
Success!
Error: Missing permission: admin

Memoization Decorator

import functools
import time
from typing import Callable, Any

def memoize(maxsize: int = 128):
    """Custom memoization decorator."""
    cache = {}
    hits = 0
    misses = 0
    
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args):
            nonlocal hits, misses
            
            if args in cache:
                hits += 1
                return cache[args]
            
            misses += 1
            result = func(*args)
            
            if len(cache) >= maxsize:
                # Simple eviction: remove first item
                cache.pop(next(iter(cache)))
            
            cache[args] = result
            return result
        
        def cache_info():
            return {
                'hits': hits,
                'misses': misses,
                'size': len(cache),
                'maxsize': maxsize
            }
        
        def cache_clear():
            nonlocal hits, misses
            cache.clear()
            hits = 0
            misses = 0
        
        wrapper.cache_info = cache_info
        wrapper.cache_clear = cache_clear
        return wrapper
    return decorator

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

# Usage
start = time.time()
result = fibonacci(100)
elapsed = time.time() - start

print(f"Fibonacci(100) = {result}")
print(f"Time: {elapsed:.4f}s")
print(f"Cache info: {fibonacci.cache_info()}")

Output:

Architecture Diagram
Fibonacci(100) = 354224848179261915075
Time: 0.0003s
Cache info: {'hits': 98, 'misses': 101, 'size': 101, 'maxsize': 100}

💡

Performance Tip: Memoization can transform O(2^n) algorithms to O(n) for recursive functions with overlapping subproblems.


Async Decorators

Async Function Decorators

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

# Async decorator
def async_retry(max_attempts: int = 3, delay: float = 1.0):
    """Retry decorator for async functions."""
    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return await func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < max_attempts - 1:
                        await asyncio.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@async_retry(max_attempts=3, delay=0.1)
async def async_unreliable_function():
    """Async function that might fail."""
    import random
    if random.random() < 0.5:
        raise ValueError("Random failure")
    return "Success!"

# Async timing decorator
def async_timer(func):
    """Time async function execution."""
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        start = time.time()
        result = await func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@async_timer
async def slow_operation():
    await asyncio.sleep(0.1)
    return "Done"

async def main():
    result = await slow_operation()
    print(result)

asyncio.run(main())

Async Class Decorators

import functools
import asyncio
from typing import Callable

class AsyncCache:
    """Async version of cache decorator."""
    
    def __init__(self, maxsize: int = 128):
        self.maxsize = maxsize
        self.cache = {}
    
    def __call__(self, func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            key = str(args) + str(kwargs)
            
            if key in self.cache:
                return self.cache[key]
            
            result = await func(*args, **kwargs)
            
            if len(self.cache) >= self.maxsize:
                self.cache.pop(next(iter(self.cache)))
            
            self.cache[key] = result
            return result
        return wrapper

@AsyncCache(maxsize=100)
async def async_expensive_computation(n):
    await asyncio.sleep(0.01)
    return n * n

async def main():
    result = await async_expensive_computation(5)
    print(f"Result: {result}")

asyncio.run(main())

ℹ️

Async Decorators: When decorating async functions, you must use async def and await in the wrapper.


Real-World Patterns

Authentication Decorator

import functools
import hashlib
import time
from typing import Callable, Any, Optional

class AuthError(Exception):
    pass

def require_auth(token: Optional[str] = None):
    """Authentication decorator."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Get token from kwargs or use default
            provided_token = kwargs.pop('auth_token', token)
            
            if not provided_token:
                raise AuthError("No authentication token provided")
            
            # Simple token validation (in real app, use proper JWT verification)
            expected_token = "secret_token_123"
            if provided_token != expected_token:
                raise AuthError("Invalid authentication token")
            
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_auth()
def get_sensitive_data(data_id: int):
    return {"id": data_id, "secret": "sensitive_data"}

# Usage
try:
    result = get_sensitive_data(1, auth_token="secret_token_123")
    print(f"Success: {result}")
except AuthError as e:
    print(f"Auth Error: {e}")

try:
    result = get_sensitive_data(1, auth_token="wrong_token")
except AuthError as e:
    print(f"Auth Error: {e}")

Rate Limiting Decorator

import functools
import time
from collections import defaultdict
from typing import Callable, Any

class RateLimiter:
    """Rate limiter using sliding window."""
    
    def __init__(self, max_calls: int, time_window: float):
        self.max_calls = max_calls
        self.time_window = time_window
        self.calls = defaultdict(list)
    
    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            key = f"{func.__name__}:{args}"
            
            # Remove old calls outside time window
            self.calls[key] = [
                call_time for call_time in self.calls[key]
                if now - call_time < self.time_window
            ]
            
            if len(self.calls[key]) >= self.max_calls:
                raise Exception(f"Rate limit exceeded for {func.__name__}")
            
            self.calls[key].append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimiter(max_calls=5, time_window=1.0)
def api_endpoint(endpoint: str):
    return f"Response from {endpoint}"

# Usage
for i in range(7):
    try:
        result = api_endpoint("/users")
        print(f"Call {i+1}: {result}")
    except Exception as e:
        print(f"Call {i+1}: {e}")

Validation Decorator

import functools
from typing import Any, Callable, Dict, List, Type

def validate(**validators):
    """Validate function arguments."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Validate positional arguments
            for i, arg in enumerate(args):
                if i in validators:
                    if not validators[i](arg):
                        raise ValueError(f"Invalid argument {i}: {arg}")
            
            # Validate keyword arguments
            for key, value in kwargs.items():
                if key in validators:
                    if not validators[key](value):
                        raise ValueError(f"Invalid argument {key}: {value}")
            
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate(
    age=lambda x: isinstance(x, int) and 0 <= x <= 150,
    email=lambda x: isinstance(x, str) and "@" in x,
    name=lambda x: isinstance(x, str) and len(x) > 0
)
def create_user(name: str, age: int, email: str):
    return {"name": name, "age": age, "email": email}

# Usage
try:
    user = create_user(name="John", age=30, email="john@example.com")
    print(f"Created user: {user}")
except ValueError as e:
    print(f"Validation error: {e}")

try:
    user = create_user(name="John", age=-5, email="john@example.com")
except ValueError as e:
    print(f"Validation error: {e}")

💡

Design Pattern: Decorators are a form of the Decorator pattern from GoF. They add behavior to objects dynamically.


Complexity Analysis

Time Complexity

Decorator TypeOverheadUse Case
Simple wrapperO(1)Logging, timing
MemoizationO(1) avgCaching
Rate limitingO(n)API throttling
ValidationO(1)Input checking

Space Complexity

DecoratorSpaceNotes
SimpleO(1)No state
MemoizationO(n)Cache storage
StatefulO(n)Instance attributes
StackingO(k)k = number of decorators

Interview Tips

Common Follow-up Questions

  1. "What's the difference between a decorator and a higher-order function?"

    • Decorators are a specific use case of higher-order functions
    • Higher-order functions take/return functions
    • Decorators specifically modify function behavior
  2. "How do you debug decorators?"

    • Use functools.wraps to preserve metadata
    • Add logging inside wrappers
    • Use __name__ and __doc__ attributes
  3. "Can decorators be used on classes?"

    • Yes, class decorators exist
    • They can modify class behavior
    • Examples: @dataclass, @singleton

Code Review Tips

# BAD: Not preserving metadata
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# GOOD: Preserving metadata
import functools
def good_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# BAD: Hardcoded parameters
def bad_retry(func):
    def wrapper(*args, **kwargs):
        for i in range(3):
            try:
                return func(*args, **kwargs)
            except:
                pass
        raise Exception("Failed")
    return wrapper

# GOOD: Parameterized
def good_retry(max_attempts=3, delay=1.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if i == max_attempts - 1:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

⚠️

Common Mistake: Forgetting to use functools.wraps causes loss of function metadata, making debugging difficult.


Summary

PatternUse CaseComplexity
Simple wrapperLogging, timingO(1)
ParameterizedConfigurable behaviorO(1)
Class-basedStateful decoratorsO(1)
StackingMultiple behaviorsO(k)
AsyncAsync function modificationO(1)
FactoryCreating decoratorsO(1)

Best Practices

  1. Always use functools.wraps to preserve metadata
  2. Use parameterized decorators for configurable behavior
  3. Consider class decorators for stateful operations
  4. Test decorators independently before applying
  5. Document decorator behavior clearly

ℹ️

Key Takeaway: Decorators are powerful metaprogramming tools. Master them to write cleaner, more maintainable code.


Practice Problems

  1. Caching Decorator: Implement an LRU cache decorator
  2. Timing Decorator: Create a decorator that logs execution time
  3. Retry Decorator: Build a retry decorator with exponential backoff
  4. Validation Decorator: Implement argument validation
  5. Rate Limiter: Create a rate limiting decorator

Further Reading

  • PEP 318: Decorators for functions and methods
  • PEP 612: Parameter specification variables
  • Books: "Python Cookbook" by David Beazley
  • Advanced: Descriptor protocol and metaclasses

Remember: Decorators are just functions that take functions and return functions. Understanding this simple concept unlocks powerful metaprogramming capabilities.

Advertisement