Decorators

What are Decorators?

Decorators are functions that modify or enhance other functions without changing their code directly.

# Basic decorator
def my_decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# Output:
# Before function call
# Hello!
# After function call

# Equivalent to:
# say_hello = my_decorator(say_hello)

Decorators with Arguments

# Decorator that handles function arguments
def timer(func):
    import time
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function(n):
    import time
    time.sleep(n)
    return f"Slept for {n} seconds"

result = slow_function(1)
print(result)

# Preserving function metadata
from functools import wraps

def better_timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        import time
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
        return result
    return wrapper

Parameterized Decorators

# Decorator that takes parameters
def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

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

greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

# Retry decorator
def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            import time
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise e
                    print(f"Attempt {attempt + 1} failed: {e}")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_function():
    import random
    if random.random() < 0.7:
        raise Exception("Random failure")
    return "Success!"

Class-Based Decorators

# Decorator as a class
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} has been called {self.count} times")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()  # say_hello has been called 1 times
say_hello()  # say_hello has been called 2 times

# Parameterized class decorator
class RateLimit:
    def __init__(self, max_calls=10, time_window=60):
        self.max_calls = max_calls
        self.time_window = time_window
        self.calls = []
    
    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            import time
            now = time.time()
            # Remove old calls outside time window
            self.calls = [call_time for call_time in self.calls 
                         if now - call_time < self.time_window]
            
            if len(self.calls) >= self.max_calls:
                raise Exception(f"Rate limit exceeded: {self.max_calls} calls per {self.time_window}s")
            
            self.calls.append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimit(max_calls=3, time_window=10)
def api_call():
    return "API response"

Common Decorator Patterns

# Authentication decorator
def requires_auth(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # In a real app, check session, token, etc.
        if not hasattr(wrapper, 'authenticated'):
            raise PermissionError("Authentication required")
        return func(*args, **kwargs)
    return wrapper

# Cache decorator
def memoize(func):
    cache = {}
    @wraps(func)
    def wrapper(*args, **kwargs):
        key = str(args) + str(sorted(kwargs.items()))
        if key not in cache:
            cache[key] = func(*args, **kwargs)
        return cache[key]
    return wrapper

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

# Validation decorator
def validate_types(**expected_types):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Get function signature
            import inspect
            sig = inspect.signature(func)
            bound_args = sig.bind(*args, **kwargs)
            bound_args.apply_defaults()
            
            for param_name, expected_type in expected_types.items():
                if param_name in bound_args.arguments:
                    value = bound_args.arguments[param_name]
                    if not isinstance(value, expected_type):
                        raise TypeError(f"{param_name} must be {expected_type.__name__}")
            
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(name=str, age=int)
def create_user(name, age):
    return f"User: {name}, Age: {age}"

Property Decorators

# Using @property, @setter, @deleter
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
    
    @radius.deleter
    def radius(self):
        print("Deleting radius")
        self._radius = 0
    
    @property
    def area(self):
        return 3.14159 * self._radius ** 2
    
    @property
    def diameter(self):
        return 2 * self._radius

circle = Circle(5)
print(circle.radius)    # 5
print(circle.area)      # 78.53975
circle.radius = 10      # Uses setter
print(circle.diameter)  # 20
del circle.radius       # Uses deleter

Stacking Decorators

# Multiple decorators on one function
@timer
@retry(max_attempts=2)
@memoize
def expensive_calculation(n):
    import time
    time.sleep(0.1)  # Simulate expensive operation
    return n ** 2

# Equivalent to:
# expensive_calculation = timer(retry(max_attempts=2)(memoize(expensive_calculation)))

result = expensive_calculation(5)
print(result)

# Order matters!
@repeat(2)
@timer
def test_function():
    print("Testing...")

# This will time each repetition separately

@timer
@repeat(2)
def test_function2():
    print("Testing...")

# This will time the entire repeated execution
💡 Decorator Best Practices:
← Error Handling Next: Generators →