Understanding Decorators
A decorator is a function that takes another function as input, adds some functionality, and returns a modified function. Decorators let you modify or extend the behavior of functions without changing their code.
The @decorator syntax is syntactic sugar for wrapping a function. Writing @my_decorator above a function definition is the same as calling my_function = my_decorator(my_function).
# A simple decorator
def uppercase_result(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase_result
def greet(name):
return f"hello, {name}"
print(greet("alice")) # HELLO, ALICE
# Decorator with timing
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} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done!"
result = slow_function()
# slow_function took 1.0012 seconds
print(result) # Done! - A decorator is a function that wraps another function
- @decorator — apply the decorator using the @ syntax
- The wrapper function calls the original and can add logic before/after
- *args, **kwargs in the wrapper make it work with any function signature
- Decorators are widely used in Flask, Django, and other frameworks
JavaScript
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f" -> returned {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def greet(name):
return f"Hello, {name}!"
add(3, 5)
greet("Alice") - Use functools.wraps in your decorators to preserve the original function's name, docstring, and other metadata.
Practical Decorators and Chaining
Decorators have many practical applications: logging, authentication, caching, rate limiting, and input validation. You can also stack multiple decorators on a single function — they are applied bottom-up.
Decorators with arguments require an extra layer of nesting — a decorator factory that returns the actual decorator.
from functools import wraps
# Decorator with arguments (decorator factory)
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(times=3)
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
# Chaining decorators
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 format_text(text):
return text
print(format_text("Hello")) # <b><i>Hello</i></b>
# Caching with built-in decorator
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # 12586269025 (instant with caching!) JavaScript
from functools import wraps
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"))
print(greet("Bob")) - When chaining decorators, the order matters. @bold @italic means bold(italic(func)). The bottommost decorator is applied first, then the one above it wraps the result.
