🎭 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
Type | Purpose | Example Use Case |
---|---|---|
Simple | Basic functionality | Logging, timing |
With arguments | Configurable behavior | Rate limiting, retries |
Class-based | Complex state | Caching, counters |
Built-in | Python 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
Pattern | Purpose | Implementation |
---|---|---|
Timing | Measure execution time | Store start/end times |
Logging | Track function calls | Print function info |
Caching | Store results | Dictionary to cache returns |
Validation | Check inputs | Type/value checking |
Rate limiting | Control call frequency | Time-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?
Track Your Learning Progress
Sign in to bookmark tutorials and keep track of your learning journey.
Your progress is saved automatically as you read.