They are widely used in real-world applications for logging, authentication, caching, validation, and more.
At their core, decorators rely on the concept that functions are first-class objects in Python, meaning they can be passed around, returned from other functions, and assigned to variables.
To understand decorators properly, you must first be comfortable with the idea of higher-order functions. A higher-order function is a function that either takes another function as an argument or returns a function.
Let us begin with a simple example of a function:
def greet():
print("Hello, World!")
Now suppose you want to add extra behavior, such as printing a message before and after the function runs, without modifying the original function. This is where decorators come in.
Creating a Basic Decorator
A decorator is essentially a function that wraps another function. It takes a function as input and returns a new function with added functionality.def my_decorator(func):
def wrapper():
print("Something before the function runs")
func()
print("Something after the function runs")
return wrapper
Now we can apply this decorator to our greet function:
greet = my_decorator(greet)
greet()
Example:
def greet():
print("Hello, World!")
def my_decorator(func):
def wrapper():
print("Something before the function runs")
func()
print("Something after the function runs")
return wrapper
greet = my_decorator(greet)
greet()
Output:
Something before the function runs
Hello, World!
Something after the function runs
Instead of manually reassigning the function, Python provides a cleaner syntax using the @decorator syntax.
This is equivalent to greet = my_decorator(greet) but is more readable and commonly used in practice.
def my_decorator(func):
def wrapper():
print("Something before the function runs")
func()
print("Something after the function runs")
return wrapper
@my_decorator
def greet():
print("Hello, World!")
greet()
Decorators with Arguments
To handle functions with different parameter lists, the wrapper function typically accepts arbitrary positional and keyword arguments using *args and **kwargs.def my_decorator(func):
def wrapper(*args, **kwargs):
print("Something before the function runs")
func(*args, **kwargs)
print("Something after the function runs")
return wrapper
@my_decorator
def printSum(a, b):
print(a + b)
printSum(1, 2)
Output:
Something before the function runs
3
Something after the function runs
This makes the decorator flexible enough to work with any function.
If you know exactly what function signature the decorator will be used with, you can define the wrapper with explicit parameters.
def my_decorator(func):
def wrapper(a, b):
print("Before")
func(a, b)
print("After")
return wrapper
@my_decorator
def printSum(a, b):
print(a + b)
printSum(1, 2)
This works perfectly because printSum() always accepts exactly two arguments. However, this decorator cannot decorate functions with a different signature:
@my_decorator
def greet(name):
print(name)
greet("John") # TypeError
Since a decorator may be applied to functions with different parameter lists, the wrapper function is typically defined using *args and **kwargs.
This allows it to accept and forward any combination of positional and keyword arguments to the original function, making the decorator reusable across a wide range of functions.
Preserving Function Metadata
When you decorate a function, you may notice that its original name and documentation are lost.This happens because the wrapper function replaces the original function. To fix this, Python provides functools.wraps.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Something before the function runs")
func(*args, **kwargs)
print("Something after the function runs")
return wrapper
@my_decorator
def printSum(a, b):
print(a + b)
print(printSum.__name__)
print(printSum.__doc__)
Using @wraps ensures that the original function's metadata is preserved.
Decorators with Arguments
Sometimes you may want to pass arguments to the decorator itself. In such cases, you need an extra level of nesting, often called a decorator factory.def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hello!")
say_hello()
Output:
Hello!
Hello!
Hello!
Here, repeat(3) returns a decorator that repeats the function execution three times.
Use Cases of Decorators
One common use case is logging, where you log function calls without modifying the function itself.def log_function(func):
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_function
def multiply(a, b):
return a * b
print(multiply(4, 5))
Output:
Calling function: multiply
20
Another important use case is authentication and authorization, especially in web frameworks, where decorators are used to restrict access to certain functions.
Decorators are also used for caching, such as in the built-in functools.lru_cache, which stores results of expensive function calls to improve performance.
from functools import lru_cache
@lru_cache(maxsize=100)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))
Chaining Multiple Decorators
Python allows multiple decorators to be applied to a single function. They are executed in the order they are listed, from bottom to top.def decorator1(func):
def wrapper():
print("Decorator 1")
func()
return wrapper
def decorator2(func):
def wrapper():
print("Decorator 2")
func()
return wrapper
@decorator1
@decorator2
def say_hi():
print("Hi!")
say_hi()
Output:
Decorator 1
Decorator 2
Hi!
Decorators are an elegant and powerful feature of Python that enable code reusability, separation of concerns, and cleaner code structure.
By wrapping functions with additional functionality, decorators allow developers to add behavior dynamically without altering the original logic.