Day 11 was about decorators in Python, which allow you to write a function that will be used to encapsulate (wrap) another function later.
The Challenge:
- Write a decorator called
timerthat:
- Records the time before and after the decorated function runs
- Prints how long the function took in milliseconds
- Returns the function’s result unchanged
- (Import time from the standard library — look up
time.time())
- Write a decorator called
loggerthat:
- Prints the function’s name and arguments every time it’s called
- Prints the return value after it runs
- Returns the result unchanged
- (Every function has a name attribute — use it)
- Write a decorator called
validate_positivethat:
- Checks all arguments passed to the function
- If any argument is zero or negative, prints a warning and returns None without calling the function
- Otherwise calls the function normally
- Apply all three decorators to a function called
calculate_discount(price, percent)that returns the discounted price
- Test it with valid inputs and with a negative number to prove
validate_positiveworks
Things to figure out: how to stack multiple decorators (order matters — think about which runs first), and how to inspect *args inside the wrapper to check values.
My Code:
import time
from functools import wraps
# 1. Timer Decorator
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
# Calculate duration in milliseconds
duration_ms = (end_time - start_time) * 1000
print(f"[Timer] {func.__name__} took {duration_ms:.4f} ms")
return result
return wrapper
# 2. Logger Decorator
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[Logger] Calling '{func.__name__}' with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[Logger] '{func.__name__}' returned: {result}")
return result
return wrapper
# 3. Validate Positive Decorator
def validate_positive(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Check all positional arguments
for arg in args:
if isinstance(arg, (int, float)) and arg <= 0:
print(
f"[Warning] Invalid argument '{arg}': all arguments must be positive."
)
return None
# Check all keyword arguments
for kwarg in kwargs.values():
if isinstance(kwarg, (int, float)) and kwarg <= 0:
print(
f"[Warning] Invalid argument '{kwarg}': all arguments must be positive."
)
return None
return func(*args, **kwargs)
return wrapper
# Apply all three decorators to the target function
# Note: Decorators are executed from top to bottom when checking,
# but they wrap from bottom to top (innermost to outermost).
@logger
@timer
@validate_positive
def calculate_discount(price, percent):
# Simulating a tiny bit of processing time so the timer registers something
time.sleep(0.01)
return price - (price * (percent / 100))
# --- Testing ---
print("--- Testing Valid Inputs ---")
final_price = calculate_discount(100, 20)
print(f"Final output captured: {final_price}")
print("\n--- Testing Negative Inputs ---")
invalid_price = calculate_discount(-50, 20)
print(f"Final output captured: {invalid_price}")
Running the Code:
--- Testing Valid Inputs --- [Logger] Calling 'calculate_discount' with args=(100, 20), kwargs={} [Timer] calculate_discount took 12.5382 ms [Logger] 'calculate_discount' returned: 80.0 Final output captured: 80.0
--- Testing Negative Inputs --- [Logger] Calling 'calculate_discount' with args=(-50, 20), kwargs={} [Warning] Invalid argument '-50': all arguments must be positive. [Timer] calculate_discount took 0.0041 ms [Logger] 'calculate_discount' returned: None Final output captured: None
I’m not going to lie, decorators have always made me scratch my head a bit. I think the nested nature of it is what causes me to second guess myself all the time. Either way, glad to be done with this one.