8 min read
ā¢Question 15 of 41mediumDecorators in Python
Understanding Python decorators.
What You'll Learn
- What decorators are and how they work
- Writing basic and parameterized decorators
- Using functools.wraps to preserve metadata
- Class decorators and decorator stacking
- Common real-world decorator patterns
Understanding Decorators
Decorators are functions that modify the behavior of other functions without changing their code. They're a powerful way to add functionality like logging, authentication, caching, and more.
The @decorator syntax is just syntactic sugar for:
code.pyPython
@decorator
def func():
pass
# Is equivalent to:
func = decorator(func)How Decorators Work
code.pyPython
# Step 1: A decorator is a function that takes a function
def my_decorator(func):
# Step 2: It defines a wrapper function
def wrapper(*args, **kwargs):
print("Before the function call")
result = func(*args, **kwargs) # Call original function
print("After the function call")
return result
# Step 3: It returns the wrapper
return wrapper
# Step 4: Apply with @ syntax
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
# Output:
# Before the function call
# Hello, Alice!
# After the function callBasic Decorator Pattern
code.pyPython
from functools import wraps
def logger(func):
@wraps(func) # Preserves original function metadata
def wrapper(*args, **kwargs):
print(f"Calling: {func.__name__}")
print(f"Args: {args}, Kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"Returned: {result}")
return result
return wrapper
@logger
def add(a, b):
"""Add two numbers."""
return a + b
add(2, 3)
# Calling: add
# Args: (2, 3), Kwargs: {}
# Returned: 5Why Use @wraps?
Without @wraps, the decorated function loses its metadata:
code.pyPython
from functools import wraps
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def hello():
"""Says hello."""
pass
print(hello.__name__) # "wrapper" (wrong!)
print(hello.__doc__) # None (lost!)
@good_decorator
def hello2():
"""Says hello."""
pass
print(hello2.__name__) # "hello2" (correct!)
print(hello2.__doc__) # "Says hello." (preserved!)Decorators with Arguments
code.pyPython
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!Multiple Decorators (Stacking)
Decorators are applied bottom-up:
code.pyPython
@decorator1
@decorator2
@decorator3
def func():
pass
# Equivalent to:
func = decorator1(decorator2(decorator3(func)))Common Real-World Decorators
Timer Decorator
code.pyPython
import time
from functools import wraps
def timer(func):
@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} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)Retry Decorator
code.pyPython
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3)
def unstable_api_call():
# May fail sometimes
passMemoization/Caching
code.pyPython
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(100) # Computed once, cached for future callsAuthentication Decorator
code.pyPython
def require_auth(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.is_authenticated:
raise PermissionError("Authentication required")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def view_dashboard(user):
return "Dashboard data"Class Decorators
Decorate classes to modify their behavior:
code.pyPython
def singleton(cls):
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
pass
db1 = Database()
db2 = Database()
print(db1 is db2) # True - same instanceInterview Tip
When asked about decorators:
- They're functions that wrap other functions
- The
@decoratorsyntax is syntactic sugar - Always use
@wrapsto preserve metadata - Know the pattern for parameterized decorators (triple-nested)
- Common uses: logging, timing, caching, auth, retry
- Apply bottom-up when stacking multiple decorators