Python Decorators
# Python Decorators
Welcome to Chapter 21! Decorators let you modify or enhance functions without changing their source code. They're used extensively in frameworks like Flask, Django, and FastAPI.
---
1. Learning Objectives
- Understand functions as first-class objects.
- Create basic decorators.
-
Use
@decoratorsyntax.
- Chain multiple decorators.
- Build practical decorators.
---
2. Functions as First-Class Objects
```python id="py21ex1" # Functions can be assigned to variables def greet(name): return f"Hello, {name}!"
sayhello = greet # Assign function to variable print(say_hello("Alice")) # Hello, Alice!
# Functions can be passed as arguments def apply(func, value): return func(value)
print(apply(len, "Python")) # 6
# Functions can return functions def multiplier(factor): def multiply(x): return x * factor return multiply
double = multiplier(2) print(double(5)) # 10
python id="py21ex2" def mydecorator(func): def wrapper(*args, kwargs): print("⏳ Before function call") result = func(*args, kwargs) print("✅ After function call") return result return wrapper
@mydecorator # Same as: greet = mydecorator(greet) def greet(name): print(f"Hello, {name}!")
greet("Alice") # Output: # ⏳ Before function call # Hello, Alice! # ✅ After function call
python id="py21ex3" import time
def timer(func): def wrapper(*args, kwargs): start = time.time() result = func(*args, kwargs) elapsed = time.time() - start print(f"⏱️ {func.name} took {elapsed:.4f}s") return result return wrapper
@timer def slowfunction(): time.sleep(1) return "Done!"
slow_function()
python id="py21ex4" from datetime import datetime
def logger(func): def wrapper(*args, kwargs): timestamp = datetime.now().strftime("%H:%M:%S") print(f"[{timestamp}] Calling {func.name}({args}, {kwargs})") result = func(*args, kwargs) print(f"[{timestamp}] {func.name_} returned {result}") return result return wrapper
@logger def add(a, b): return a + b
add(3, 5)
python id="py21ex5" def requireauth(func): def wrapper(user, *args, kwargs): if not user.get("isauthenticated"): print("❌ Access denied! Please login.") return None return func(user, *args, kwargs) return wrapper
@requireauth def viewdashboard(user): print(f"Welcome to dashboard, {user['name']}! 🎉")
viewdashboard({"name": "Alice", "isauthenticated": True}) viewdashboard({"name": "Bob", "is_authenticated": False})
python id="py21ex6" def repeat(times): def decorator(func): def wrapper(*args, kwargs): for in range(times): result = func(*args, kwargs) return result return wrapper return decorator
@repeat(3) def sayhello(): print("Hello!")
sayhello() # Prints "Hello!" three times
python id="py21ex7" from functools import wraps
def mydecorator(func): @wraps(func) # Preserves original function's name and docstring def wrapper(*args, kwargs): return func(*args, kwargs) return wrapper
@mydecorator def greet(name): """Greet a user by name.""" return f"Hello, {name}!"
print(greet.name) # greet (not 'wrapper') print(greet.doc_) # Greet a user by name.
python id="py21_ex8" def bold(func): @wraps(func) def wrapper(*args, kwargs): return f"<b>{func(*args, kwargs)}</b>" return wrapper
def italic(func): @wraps(func) def wrapper(*args, kwargs): return f"<i>{func(*args, kwargs)}</i>" return wrapper
@bold @italic def greet(name): return f"Hello, {name}!"
print(greet("Alice")) # <b><i>Hello, Alice!</i></b> # Applied bottom-up: italic first, then bold
python id="py21ex9" class CountCalls: def init(self, func): self.func = func self.count = 0 def call(self, *args, kwargs): self.count += 1 print(f"📊 {self.func.name} called {self.count} times") return self.func(*args, kwargs)
@CountCalls def sayhi(): print("Hi!")
sayhi() # called 1 times
sayhi() # called 2 times
sayhi() # called 3 times
``
---
9. MCQs with Answers
Q1: @decorator is equivalent to:
A) func = decorator B) func = decorator(func) C) decorator = func D) func(decorator)
Answer: B
Q2: Decorators modify functions: A) By editing source code B) By wrapping them C) By deleting them D) By copying them Answer: B
Q3: functools.wraps preserves:
A) Speed B) Function metadata C) Memory D) Arguments
Answer: B
Q4: Decorator with arguments needs: A) 1 level of nesting B) 2 levels C) 3 levels D) No nesting Answer: C — outer(args) → decorator(func) → wrapper(*args).
Q5: Chained decorators apply: A) Top to bottom B) Bottom to top C) Random D) Parallel Answer: B — Innermost decorator applies first.
Q6: *args, kwargs in wrapper ensures:
A) Decorator works with any function signature B) Speed C) Type safety D) Memory
Answer: A
Q7: Class-based decorators use:
A) init and call B) str C) iter D) get
Answer: A
Q8: Real-world use of decorators: A) Flask routes B) Django views C) pytest fixtures D) All of these Answer: D
Q9: Can decorators be stacked? A) Yes B) No C) Only 2 D) Only 3 Answer: A
Q10: Without @wraps, func.name returns:
A) Original name B) 'wrapper' C) None D) Error
Answer: B
---
10. Interview Questions
- 1. What is a decorator? A function that takes a function and returns a modified version, adding behavior without changing the original code.
-
2.
Why use @wraps
? Preserves the original function'sname,doc, and other metadata.
-
3.
Real-world decorator examples? @app.route()
(Flask),@loginrequired(Django),@property,@staticmethod,@classmethod.
- 4. Decorator with arguments pattern? Triple-nested: decorator_factory(args) → decorator(func) → wrapper(*args, kwargs).
- 5. Can decorators modify return values? Yes, the wrapper function can transform the return value before returning it.
---
11. Summary
- Decorators wrap functions to add behavior without modifying source code.
-
Use @decorator
syntax for clean application.
- Always use @functools.wraps` to preserve metadata.
- Decorators can accept arguments (triple-nested pattern).
- Multiple decorators can be chained (applied bottom-up).
---
12. Next Chapter Recommendation
In Chapter 22: Working with Dates and Time, you'll master the datetime module for time-based operations! 🚀