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. This concept is the foundation of how decorators work.
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.
@my_decorator
def greet():
print("Hello, World!")
greet()
This is equivalent to greet = my_decorator(greet) but is more readable and commonly used in practice.
Example:
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()
Output:
Something before the function runs
Hello, World!
Something after the function runs
Decorators with Arguments
The previous example works only with functions that take no arguments. In real-world scenarios, functions usually accept parameters. To handle this, the wrapper function must accept arbitrary 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.
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 (Decorator Factory)
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.
Practical Use Cases of Decorators
Decorators are heavily used in real-world Python applications. 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.
Join the discussion