Decorator Fundamentals¶
A decorator is a function that takes another function as input and returns a modified version of it. Decorators provide a clean syntax for wrapping functions with additional behavior.
Mental Model
Think of a decorator as gift-wrapping a function: the original function is still inside, but the wrapper adds behavior before and after each call. The @decorator syntax is just shorthand for func = decorator(func) -- it replaces the name with the wrapped version.
Basic Syntax¶
The @ Syntax¶
```python def decorator(func): def wrapper(args, kwargs): print("Before") result = func(args, **kwargs) print("After") return result return wrapper
@decorator def greet(): print("Hello")
Equivalent to:¶
greet = decorator(greet)¶
```
The @decorator syntax is syntactic sugar—it automatically passes the function to the decorator and reassigns the result.
Calling Decorated Functions¶
```python greet()
Output:¶
Before¶
Hello¶
After¶
```
The Wrapper Pattern¶
Basic Structure¶
python
def decorator(func):
def wrapper(*args, **kwargs):
# Before logic (pre-processing)
result = func(*args, **kwargs)
# After logic (post-processing)
return result
return wrapper
Key elements:
func: The original function being decoratedwrapper: The new function that wraps the original*args, **kwargs: Accepts any arguments to pass throughreturn result: Preserves the original return value
Why *args, **kwargs?¶
```python def decorator(func): def wrapper(args, kwargs): # Accept ANY arguments return func(args, **kwargs) # Pass them through return wrapper
@decorator def add(a, b): return a + b
@decorator def greet(name, greeting="Hello"): return f"{greeting}, {name}!"
Both work with the same decorator¶
add(2, 3) # 5 greet("Alice") # "Hello, Alice!" ```
Execution Time¶
Decorators Run at Definition Time¶
```python def decorator(func): print(f"Decorating {func.name}") # Runs immediately! return func
@decorator # Prints "Decorating function" when this line executes def function(): pass
The decorator has already run before we call function()¶
```
This is important: the decorator itself runs when Python loads the module, not when you call the decorated function.
Preserving Function Metadata¶
The Problem¶
```python def decorator(func): def wrapper(args, kwargs): return func(args, **kwargs) return wrapper
@decorator def greet(): """Say hello.""" print("Hello")
print(greet.name) # 'wrapper' — Wrong! print(greet.doc) # None — Lost! ```
The Solution: functools.wraps¶
```python from functools import wraps
def decorator(func): @wraps(func) # Copies metadata from func to wrapper def wrapper(args, kwargs): return func(args, **kwargs) return wrapper
@decorator def greet(): """Say hello.""" print("Hello")
print(greet.name) # 'greet' — Correct! print(greet.doc) # 'Say hello.' — Preserved! ```
Always use @wraps—it preserves:
__name__: Function name__doc__: Docstring__module__: Module name__annotations__: Type hints__dict__: Function attributes
State in Decorators¶
Using Closure Variables¶
```python from functools import wraps
def count_calls(func): count = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(f"Call #{count}")
return func(*args, **kwargs)
return wrapper
@count_calls def greet(): print("Hello")
greet() # Call #1, Hello greet() # Call #2, Hello ```
Using Function Attributes¶
```python from functools import wraps
def count_calls(func): @wraps(func) def wrapper(args, kwargs): wrapper.calls += 1 return func(args, **kwargs)
wrapper.calls = 0
return wrapper
@count_calls def greet(): print("Hello")
greet() greet() print(greet.calls) # 2 — Accessible from outside ```
Closures vs Decorators¶
Closure¶
A function that retains access to variables from its enclosing scope:
```python def make_multiplier(factor): def multiply(x): return x * factor # 'factor' captured from enclosing scope return multiply
times3 = make_multiplier(3) print(times3(10)) # 30 ```
Decorator¶
A function that takes another function as input and returns a new function:
python
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
Relationship¶
Decorators are built on closures. The wrapper function inside a decorator is a closure—it captures func from the enclosing scope.
Comparison¶
| Feature | Closure | Decorator |
|---|---|---|
| Purpose | Retain state from outer function | Modify/enhance function behavior |
| Returns | A nested function | A new function wrapping the original |
| Captures | Variables from enclosing scope | The decorated function |
| Used for | Function factories, stateful functions | Logging, timing, caching, validation |
When to Use Each¶
Closures for function factories:
```python def make_power(exp): def power(base): return base ** exp return power
square = make_power(2) cube = make_power(3) ```
Decorators for cross-cutting concerns:
```python @timer def slow_function(): ...
@cache def expensive_computation(x): ... ```
Common Decorator Template¶
```python from functools import wraps
def decorator_name(func): @wraps(func) def wrapper(args, *kwargs): # === BEFORE the original function === # Pre-processing, validation, logging, etc.
# === CALL the original function ===
result = func(*args, **kwargs)
# === AFTER the original function ===
# Post-processing, cleanup, etc.
return result
return wrapper
```
Summary¶
| Concept | Description |
|---|---|
@decorator |
Syntactic sugar for func = decorator(func) |
| Wrapper pattern | Inner function that wraps the original |
*args, **kwargs |
Accept and pass through any arguments |
@wraps(func) |
Preserve original function's metadata |
| Execution time | Decorator runs at definition, wrapper runs at call |
| Closure | Function + captured environment |
Key Takeaways:
- Decorators are functions that transform functions
- Always use
@wrapsto preserve metadata - The wrapper pattern is the foundation of most decorators
- Decorators run at definition time, not call time
- Decorators use closures internally
Runnable Example: decorator_examples.py¶
```python """ Python Decorators - Practical Examples This file contains various practical examples of decorators in Python. Run this file to see decorators in action! """
import time import functools
if name == "main":
print("=" * 70)
print("PYTHON DECORATORS - EXAMPLES")
print("=" * 70)
# ============================================================================
# EXAMPLE 1: Basic Decorator
# ============================================================================
print("\n1. BASIC DECORATOR")
print("-" * 70)
def simple_decorator(func):
@functools.wraps(func)
def wrapper():
print("Before function call")
result = func()
print("After function call")
return result
return wrapper
@simple_decorator
def say_hello():
print("Hello, World!")
return "Greeting complete"
result = say_hello()
print(f"Return value: {result}")
# ============================================================================
# EXAMPLE 2: Decorator with Function Arguments
# ============================================================================
print("\n\n2. DECORATOR WITH FUNCTION ARGUMENTS")
print("-" * 70)
def logger(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with:")
print(f" args: {args}")
print(f" kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f" result: {result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
@logger
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
add(5, 3)
print()
greet("Alice", greeting="Hi")
# ============================================================================
# EXAMPLE 3: Timing Decorator
# ============================================================================
print("\n\n3. TIMING DECORATOR")
print("-" * 70)
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} executed in {end - start:.4f} seconds")
return result
return wrapper
@timer
def fast_function():
return sum(range(100))
@timer
def slow_function():
time.sleep(0.5)
return sum(range(1000000))
print(f"Fast function result: {fast_function()}")
print(f"Slow function result: {slow_function()}")
# ============================================================================
# EXAMPLE 4: Decorator with Parameters
# ============================================================================
print("\n\n4. DECORATOR WITH PARAMETERS")
print("-" * 70)
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for i in range(times):
print(f" Execution {i+1}/{times}:")
result = func(*args, **kwargs)
results.append(result)
return results
return wrapper
return decorator
@repeat(times=3)
def greet(name):
greeting = f"Hello, {name}!"
print(f" {greeting}")
return greeting
print("Calling decorated function:")
results = greet("Bob")
print(f"All results: {results}")
# ============================================================================
# EXAMPLE 5: Multiple Decorators (Stacking)
# ============================================================================
print("\n\n5. STACKING MULTIPLE DECORATORS")
print("-" * 70)
def uppercase(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def exclaim(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + "!!!"
return wrapper
def quote(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f'"{result}"'
return wrapper
@quote
@uppercase
@exclaim
def greet(name):
return f"hello, {name}"
print(greet("Alice"))
print("Note: Decorators are applied bottom-to-top")
# ============================================================================
# EXAMPLE 6: Caching/Memoization Decorator
# ============================================================================
print("\n\n6. CACHING/MEMOIZATION DECORATOR")
print("-" * 70)
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache:
print(f" Returning cached result for {args}")
return cache[args]
print(f" Computing result for {args}")
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print("Calculating fibonacci(5):")
print(f"Result: {fibonacci(5)}")
print("\nCalculating fibonacci(5) again:")
print(f"Result: {fibonacci(5)}")
# ============================================================================
# EXAMPLE 7: Authentication/Authorization Decorator
# ============================================================================
print("\n\n7. AUTHENTICATION DECORATOR")
print("-" * 70)
# Simulated user system
current_user = {"name": "Alice", "role": "admin"}
def require_auth(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if current_user is None:
print("Access denied: Not logged in")
return None
return func(*args, **kwargs)
return wrapper
def require_role(role):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if current_user.get("role") != role:
print(f"Access denied: Requires '{role}' role")
return None
return func(*args, **kwargs)
return wrapper
return decorator
@require_auth
def view_profile():
return f"Viewing profile for {current_user['name']}"
@require_role("admin")
def delete_user(username):
return f"User {username} deleted by {current_user['name']}"
print(view_profile())
print(delete_user("Bob"))
# Change user role
current_user["role"] = "user"
print("\nAfter changing role to 'user':")
print(delete_user("Bob"))
# ============================================================================
# EXAMPLE 8: Validation Decorator
# ============================================================================
print("\n\n8. VALIDATION DECORATOR")
print("-" * 70)
def validate_positive(func):
@functools.wraps(func)
def wrapper(n):
if n < 0:
raise ValueError(f"Argument must be positive, got {n}")
return func(n)
return wrapper
def validate_range(min_val, max_val):
def decorator(func):
@functools.wraps(func)
def wrapper(n):
if not (min_val <= n <= max_val):
raise ValueError(f"Argument must be between {min_val} and {max_val}")
return func(n)
return wrapper
return decorator
@validate_positive
def square_root(n):
return n ** 0.5
@validate_range(0, 100)
def percentage_to_grade(score):
if score >= 90: return "A"
if score >= 80: return "B"
if score >= 70: return "C"
return "F"
print(f"Square root of 16: {square_root(16)}")
print(f"Grade for 85: {percentage_to_grade(85)}")
print("\nTrying invalid inputs:")
try:
square_root(-4)
except ValueError as e:
print(f" Error: {e}")
try:
percentage_to_grade(150)
except ValueError as e:
print(f" Error: {e}")
# ============================================================================
# EXAMPLE 9: Retry Decorator
# ============================================================================
print("\n\n9. RETRY DECORATOR")
print("-" * 70)
def retry(max_attempts=3, delay=0.5):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts == max_attempts:
print(f" Failed after {max_attempts} attempts")
raise
print(f" Attempt {attempts} failed: {e}. Retrying...")
time.sleep(delay)
return wrapper
return decorator
# Simulate unreliable function
call_count = 0
@retry(max_attempts=3, delay=0.1)
def unreliable_function():
global call_count
call_count += 1
print(f" Attempt {call_count}")
if call_count < 2:
raise ConnectionError("Network error")
return "Success!"
print("Calling unreliable function:")
result = unreliable_function()
print(f"Final result: {result}")
# ============================================================================
# EXAMPLE 10: Class-Based Decorator
# ============================================================================
print("\n\n10. CLASS-BASED DECORATOR")
print("-" * 70)
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count} to {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello(name):
return f"Hello, {name}!"
print(say_hello("Alice"))
print(say_hello("Bob"))
print(say_hello("Charlie"))
print(f"\nTotal calls: {say_hello.count}")
# ============================================================================
# EXAMPLE 11: Debug Decorator
# ============================================================================
print("\n\n11. DEBUG DECORATOR")
print("-" * 70)
def debug(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Calling {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"{func.__name__!r} returned {result!r}")
return result
return wrapper
@debug
def calculate(x, y, operation="+"):
if operation == "+":
return x + y
elif operation == "*":
return x * y
return None
calculate(5, 3, operation="+")
calculate(5, 3, operation="*")
# ============================================================================
# EXAMPLE 12: Rate Limiting Decorator
# ============================================================================
print("\n\n12. RATE LIMITING DECORATOR")
print("-" * 70)
def rate_limit(max_calls, time_period):
"""Limit function calls to max_calls per time_period seconds"""
def decorator(func):
calls = []
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Remove old calls outside the time window
calls[:] = [call for call in calls if now - call < time_period]
if len(calls) >= max_calls:
wait_time = time_period - (now - calls[0])
print(f" Rate limit reached. Wait {wait_time:.2f}s")
return None
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, time_period=2)
def api_call(endpoint):
return f"Calling {endpoint}"
print("Making API calls (max 3 per 2 seconds):")
for i in range(5):
result = api_call(f"endpoint_{i}")
if result:
print(f" {result}")
time.sleep(0.3)
print("\n" + "=" * 70)
print("END OF EXAMPLES")
print("=" * 70)
```
Exercises¶
Exercise 1.
Write a decorator uppercase_result that converts the return value of a function to uppercase. Use @wraps to preserve metadata. Apply it to a function def greet(name: str) -> str that returns f"hello, {name}" and verify the output.
Solution to Exercise 1
from functools import wraps
def uppercase_result(func):
"""Decorator that converts the return value to uppercase."""
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase_result
def greet(name: str) -> str:
"""Return a greeting."""
return f"hello, {name}"
print(greet("alice")) # HELLO, ALICE
print(greet.__name__) # greet
print(greet.__doc__) # Return a greeting.
Exercise 2.
Create a decorator call_counter that tracks how many times a function has been called. Store the count as a function attribute calls on the wrapper so it can be inspected from outside. Demonstrate that calling the decorated function three times sets func.calls to 3.
Solution to Exercise 2
from functools import wraps
def call_counter(func):
"""Decorator that counts how many times the function is called."""
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.calls += 1
return func(*args, **kwargs)
wrapper.calls = 0
return wrapper
@call_counter
def say_hi():
print("Hi!")
say_hi()
say_hi()
say_hi()
print(say_hi.calls) # 3
Exercise 3.
Write a decorator require_positive that inspects all positional arguments before calling the original function and raises a ValueError if any argument is a negative number. Non-numeric arguments should be ignored. Apply it to a function def area(width, height) and test with both valid and invalid inputs.
Solution to Exercise 3
from functools import wraps
def require_positive(func):
"""Decorator that rejects negative numeric arguments."""
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(
f"Negative argument not allowed: {arg}"
)
return func(*args, **kwargs)
return wrapper
@require_positive
def area(width, height):
return width * height
print(area(5, 10)) # 50
try:
area(-3, 10) # ValueError
except ValueError as e:
print(e)