🎭 Creating Decorators

Decorators are a powerful way to modify or enhance functions without changing their code. They're like adding special features to functions - timing, logging, authentication, and more - in a clean, reusable way.

# Simple decorator example
def my_decorator(func):
    def wrapper():
        print("Before function call")
        result = func()
        print("After function call")
        return result
    return wrapper

@my_decorator
def say_hello():
    print("Hello, World!")
    return "Done"

# Using the decorated function
result = say_hello()

🎯 Understanding Decorators

Decorators are functions that take another function as input and return a modified version of that function.

Simple Decorator Examples

# Timing decorator
import time

def timer(func):
    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():
    time.sleep(1)
    return "Finished sleeping"

@timer
def quick_function():
    return sum(range(1000))

# Test the decorated functions
result1 = slow_function()
result2 = quick_function()

Logging Decorator

def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        print(f"Arguments: args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"Function returned: {result}")
        return result
    return wrapper

@log_calls
def add_numbers(a, b):
    return a + b

@log_calls
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

# Test the decorated functions
sum_result = add_numbers(5, 3)
message = greet("Alice", greeting="Hi")

📋 Decorator Types Reference

TypePurposeExample Use Case
SimpleBasic functionalityLogging, timing
With argumentsConfigurable behaviorRate limiting, retries
Class-basedComplex stateCaching, counters
Built-inPython features@property, @staticmethod

🔧 Decorators with Arguments

Parameterized Decorators

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

def validate_types(**expected_types):
    def decorator(func):
        def wrapper(*args, **kwargs):
            # Simple type checking
            for key, expected_type in expected_types.items():
                if key in kwargs:
                    if not isinstance(kwargs[key], expected_type):
                        raise TypeError(f"{key} must be {expected_type.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

# Using parameterized decorators
@repeat(3)
def say_hi():
    print("Hi there!")

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

# Test the functions
say_hi()  # Prints "Hi there!" 3 times

user = create_user(name="Alice", age=30)  # Works
# user = create_user(name="Alice", age="30")  # Would raise TypeError

Conditional Decorator

def debug_mode(enabled=True):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if enabled:
                print(f"DEBUG: Calling {func.__name__} with args={args}, kwargs={kwargs}")
            result = func(*args, **kwargs)
            if enabled:
                print(f"DEBUG: {func.__name__} returned {result}")
            return result
        return wrapper
    return decorator

# Toggle debug mode
@debug_mode(enabled=True)
def calculate(x, y, operation="add"):
    if operation == "add":
        return x + y
    elif operation == "multiply":
        return x * y
    return None

@debug_mode(enabled=False)
def silent_function(value):
    return value * 2

# Test functions
result1 = calculate(5, 3)
result2 = calculate(4, 7, operation="multiply")
result3 = silent_function(10)

Error Handling Decorator

def handle_errors(default_return=None):
    def decorator(func):
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                print(f"Error in {func.__name__}: {e}")
                return default_return
        return wrapper
    return decorator

@handle_errors(default_return="Error occurred")
def divide_numbers(a, b):
    return a / b

@handle_errors(default_return=[])
def process_data(data):
    return [x * 2 for x in data]

# Test error handling
result1 = divide_numbers(10, 2)    # Normal: 5.0
result2 = divide_numbers(10, 0)    # Error: "Error occurred"

result3 = process_data([1, 2, 3])  # Normal: [2, 4, 6]
result4 = process_data("invalid")  # Error: []

📊 Common Decorator Patterns

PatternPurposeImplementation
TimingMeasure execution timeStore start/end times
LoggingTrack function callsPrint function info
CachingStore resultsDictionary to cache returns
ValidationCheck inputsType/value checking
Rate limitingControl call frequencyTime-based restrictions

🏪 Class-Based Decorators

class CallCounter:
    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)

class SimpleCache:
    def __init__(self, func):
        self.func = func
        self.cache = {}
    
    def __call__(self, *args):
        if args in self.cache:
            print(f"Cache hit for {args}")
            return self.cache[args]
        
        result = self.func(*args)
        self.cache[args] = result
        print(f"Cache miss for {args}, storing result")
        return result

# Using class-based decorators
@CallCounter
def greet(name):
    return f"Hello, {name}!"

@SimpleCache
def expensive_calculation(n):
    return sum(range(n))

# Test the decorators
msg1 = greet("Alice")  # Called 1 time
msg2 = greet("Bob")    # Called 2 times

calc1 = expensive_calculation(1000)  # Cache miss
calc2 = expensive_calculation(1000)  # Cache hit

Multiple Decorators

def bold(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return f"**{result}**"
    return wrapper

def italic(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return f"*{result}*"
    return wrapper

def uppercase(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

# Stack multiple decorators
@bold
@italic
@uppercase
def format_text(text):
    return text

# Test stacked decorators
formatted = format_text("hello world")
print(formatted)  # **_HELLO WORLD_**

🔄 Preserving Function Information

import functools

def better_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@better_decorator
def example_function():
    """This is an example function."""
    return "Hello!"

# Function information is preserved
print(f"Name: {example_function.__name__}")
print(f"Doc: {example_function.__doc__}")

🎯 Key Takeaways

🚀 What's Next?

Learn about generators to create memory-efficient iterators with yield.

Continue to: Use Generators

Was this helpful?

😔Poor
🙁Fair
😊Good
😄Great
🤩Excellent