Day 17: Decorators — Modifying Functions Elegantly
🐍 Day 17: Decorators — Modifying Functions Elegantly
1. Learning Objectives
By the end of Day 17, you will be able to:
- Explain what decorators are and why they're used
- Write a simple decorator using the
@syntax - Understand how functions are first-class objects in Python
- Apply multiple decorators to a single function
- Create decorators that accept arguments
- Recognize and use built-in decorators:
@staticmethod,@classmethod,@property - Use decorators in real-world scenarios: timing, logging, access control
2. Concept Explanation
2.1 Why Decorators? — Adding Behaviour Without Touching Code
Sometimes you need to add the same behaviour (logging, timing, authentication) to many functions. Instead of duplicating code inside every function, decorators let you wrap a function with extra logic — transparently and cleanly.
# Without decorators — repetitive
def add(a, b):
log("add called")
return a + b
def multiply(a, b):
log("multiply called")
return a * b
# With decorators — behaviour extracted
@log_function_call
def add(a, b):
return a + b
2.2 Functions Are First-Class Objects
To understand decorators, you must understand that in Python, functions are objects — you can pass them around, assign them to variables, and even define them inside other functions.
def greet(name):
return f"Hello, {name}"
# Assign function to a variable
say = greet
print(say("Alice")) # Hello, Alice
# Pass function as an argument
def execute(func, value):
return func(value)
print(execute(greet, "Bob")) # Hello, Bob
# Define function inside a function
def outer():
def inner():
return "Inside!"
return inner()
print(outer()) # Inside!
2.3 The Basic Decorator Pattern
A decorator is a function that takes another function as an argument, adds behaviour, and returns a new function.
def my_decorator(func):
def wrapper():
print("Something before the function.")
func()
print("Something after the function.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
Something before the function.
Hello!
Something after the function.
What's happening behind the scenes:
# @my_decorator is equivalent to:
say_hello = my_decorator(say_hello)
2.4 Handling Arguments — *args and **kwargs
Real functions take arguments. A robust decorator uses *args and **kwargs to pass them through.
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
add(3, 5)
# Calling add with (3, 5), {}
# add returned 8
2.5 Preserving Metadata — functools.wraps
When you wrap a function with a decorator, the original function's metadata (name, docstring) is lost. Use @functools.wraps(func) inside your wrapper to preserve it.
import functools
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper docstring."""
return func(*args, **kwargs)
return wrapper
@decorator
def greet():
"""Original docstring."""
pass
print(greet.__name__) # greet (not wrapper)
print(greet.__doc__) # Original docstring.
💡 Always use
@functools.wrapsin your decorators unless you specifically want to change the name.
2.6 Decorators with Arguments — The Three‑Layer Cake
When a decorator itself needs arguments (e.g., @retry(3)), you need a decorator factory — a function that returns a decorator.
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")
say_hi() # Prints "Hi!" three times
2.7 Built‑in Decorators You Already Know
| Decorator | Purpose | Example |
|---|---|---|
@staticmethod | Method that doesn't need self | def utils(): |
@classmethod | Method that receives cls instead of self | def factory(cls): |
@property | Turns a method into an attribute with optional getter/setter | def full_name(self): |
2.8 Common Mistakes
| Mistake | Consequence |
|---|---|
Forgetting return wrapper in the decorator | Function becomes None |
Not using *args, **kwargs | Decorated function cannot accept arguments |
Missing @functools.wraps | Loses original function name, docstring |
| Confusing decorator with argument syntax | A decorator with arguments is a function returning a decorator |
3. Code Examples
Example 1: Timer Decorator
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(0.1)
slow_function() # slow_function took 0.1003s
Example 2: Retry Decorator
import functools
def retry(max_attempts=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"Attempt {attempt} failed: {e}")
raise last_exception
return wrapper
return decorator
@retry(3)
def unstable():
import random
if random.random() < 0.7:
raise ValueError("Bad luck!")
return "Success"
Example 3: Access Control Decorator
def require_auth(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("authenticated"):
raise PermissionError("User not authenticated")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def view_dashboard(user):
return f"Welcome, {user['name']}!"
admin = {"name": "Alice", "authenticated": True}
guest = {"name": "Bob", "authenticated": False}
print(view_dashboard(admin)) # Welcome, Alice!
# view_dashboard(guest) # PermissionError
4. Hands-On Exercises
Exercise 1: Logger Decorator
Write a decorator @log_it that prints "Calling function_name" before the function runs and "Finished function_name" after it runs. Preserve metadata.
Exercise 2: Upper‑Output Decorator
Write a decorator @upper_return that takes the string returned by the decorated function and converts it to uppercase. Test with a function that returns "hello world".
Exercise 3: Timer with Threshold
Write a decorator @timer(threshold) that prints a warning only if the decorated function takes longer than threshold seconds.
Exercise 4: Memoization Cache
Write a decorator @cache_result that stores function results in a dictionary so repeated calls with the same arguments return instantly (no recalculation). Test with a recursive Fibonacci.
Exercise 5: Multiple Decorators
Apply two decorators to a function: @upper_return and @log_it. Observe the order they execute.
5. Applied Challenge Task 🏗️
Decorator‑Based Web Request Simulator
Build a mini system that simulates API requests using decorators.
Requirements:
Decorator
@authenticate(required=True):- Checks if a
userkeyword argument has"authenticated": True. - If not, raises
PermissionError. - If
required=False, allows unauthenticated but logs a warning.
- Checks if a
Decorator
@rate_limit(max_calls=5):- Keeps a counter per function.
- If the function is called more than
max_callstimes, raisesRuntimeError.
Decorator
@log_request:- Logs the function name, arguments, result, and timestamp to a file
requests.log.
- Logs the function name, arguments, result, and timestamp to a file
Simulate endpoints:
@authenticate(required=True) @rate_limit(max_calls=3) @log_request def get_user_data(user): return {"id": 1, "name": user.get("name")}Test the system with authenticated and unauthenticated users, exceeding rate limits, and verify the log file.
Stretch goals:
- Combine all three into a single composite decorator
@api_endpoint - Add a
@cache_responsedecorator that caches responses based on arguments - Unit test your decorators (preview of Day 22)
6. Brief Review Summary
| Concept | Key Points |
|---|---|
| First‑class functions | Functions are objects; can be passed and returned |
| Decorator | Function taking a function, returning a new one |
@decorator | Syntactic sugar for func = decorator(func) |
*args, **kwargs | Pass through arbitrary arguments |
functools.wraps | Preserves original function metadata |
| Decorator with arguments | Triple‑layer: decorator factory returns a decorator |
| Built‑ins | @staticmethod, @classmethod, @property |
7. Preview of Next Topic — Day 18
Tomorrow we'll complete Context Managers and dive deeper into the with statement:
- Writing context managers using
__enter__and__exit__ - The
contextlibmodule and@contextmanagerdecorator - Managing resources like files, database connections, and locks
- Creating your own context managers for custom setup/teardown logic
🎯 Your Action Items for Day 17:
- ✅ Complete all 5 exercises
- ✅ Build the Decorator‑Based Web Request Simulator
- ✅ Experiment with ordering of multiple decorators
- ✅ Compare
@propertywith regular decorator patterns
Comments
Post a Comment
Leave us your comments here...