
Python
Decorators are functions that modify other functions without changing their original code.
A decorator is a function that:
1. Takes a function as input
2. Wraps it with additional behavior
3. Returns the enhanced 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@decorator_name
def function_name():
passdef function_name():
pass
function_name = decorator_name(function_name)# Step 1: Define the decorator
def simple_decorator(func):
def wrapper():
print("Before")
func()
print("After")
return wrapper
# Step 2: Apply the decorator
@simple_decorator
def my_function():
print("During")
# Step 3: Call the decorated function
my_function()
# Output:
# Before
# During
# After@simple_decorator
def greet(name):
print(f"Hello {name}!")
greet("Alice") # ❌ Error! wrapper() doesn't accept argumentsProblem: The wrapper doesn't accept arguments. Fix with `*args` and `**kwargs`:
def simple_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@simple_decorator
def greet(name):
return f"Hello {name}!"
print(greet("Alice"))
# Output:
# Before
# After
# Hello Alice!def log_calls(func):
"""Log when a function is called."""
def wrapper(*args, **kwargs):
print(f"[LOG] Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(5, 3)
# Output:
# [LOG] Calling add with args=(5, 3), kwargs={}
# [LOG] add returned 8import time
def timing_decorator(func):
"""Measure how long a function takes."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timing_decorator
def slow_function(delay):
time.sleep(delay)
return "Done!"
slow_function(0.5)
# Output:
# slow_function took 0.5001 secondsdef handle_errors(func):
"""Catch and handle errors gracefully."""
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error in {func.__name__}: {e}")
return None
return wrapper
@handle_errors
def divide(a, b):
return a / b
divide(10, 2) # Returns 5.0
divide(10, 0) # Error: division by zero, returns Nonedef cache_result(func):
"""Cache function results to avoid recomputation."""
cache = {}
def wrapper(*args):
if args not in cache:
print(f"Computing {func.__name__}{args}...")
cache[args] = func(*args)
else:
print(f"Using cached result for {func.__name__}{args}")
return cache[args]
return wrapper
@cache_result
def expensive_function(n):
return n ** n
print(expensive_function(5)) # Computing expensive_function(5)... 3125
print(expensive_function(5)) # Using cached result... 3125You can apply multiple decorators to the same function:
def decorator_a(func):
def wrapper(*args, **kwargs):
print("A1")
result = func(*args, **kwargs)
print("A2")
return result
return wrapper
def decorator_b(func):
def wrapper(*args, **kwargs):
print("B1")
result = func(*args, **kwargs)
print("B2")
return result
return wrapper
@decorator_a
@decorator_b
def my_function():
print("Function")
my_function()
# Output:
# A1
# B1
# Function
# B2
# A2def require_login(func):
"""Decorator to check if user is logged in."""
def wrapper(*args, **kwargs):
user = kwargs.get("user")
if not user:
print("Access denied: Not logged in")
return None
print(f"Access granted for {user}")
return func(*args, **kwargs)
return wrapper
@require_login
def view_profile(**kwargs):
return f"Profile data: {kwargs['user']}"
view_profile(user="Alice") # Access granted... Profile data: Alice
view_profile() # Access deniedimport time
def rate_limit(max_calls, time_window):
"""Limit how often a function can be called."""
def decorator(func):
calls = []
def wrapper(*args, **kwargs):
now = time.time()
calls[:] = [c for c in calls if c > now - time_window]
if len(calls) >= max_calls:
print(f"Rate limit exceeded for {func.__name__}")
return None
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, time_window=10)
def api_call():
print("API call successful")
return "data"
# Can call 3 times per 10 secondsWhen you decorate a function, you lose its metadata:
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
"""This is my function."""
pass
print(my_function.__name__) # ❌ wrapper (not my_function!)
print(my_function.__doc__) # ❌ None (not the docstring!)Solution: Use `functools.wraps`:
from functools import wraps
def my_decorator(func):
@wraps(func) # ← Preserves metadata
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
"""This is my function."""
pass
print(my_function.__name__) # ✅ my_function
print(my_function.__doc__) # ✅ This is my function.| Concept | Remember |
|---|---|
| Decorator | Function that wraps another function |
| Syntax | `@decorator_name` above function |
| Purpose | Add behavior without changing original code |
| Parameters | Use `*args, **kwargs` for flexibility |
| Stacking | Can apply multiple decorators |
| Metadata | Use `@wraps` to preserve function info |
Now explore functional programming with map, filter, and reduce to work with data in elegant ways.
Next: Functional Programming →
Practice: Decorator challenges
Resources
Ojasa Mirai
Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.
Learn Deep • Build Real • Verify Skills • Launch Forward