Python decorators are a powerful and expressive feature that allows you to modify functions and methods in a clean and readable way. They provide a concise syntax for wrapping additional functionality around an existing function without modifying its core behavior. This blog post will look at decorators in detail, providing clear explanations and practical examples.
Understanding the Basics
At its heart, a decorator is a higher-order function—a function that takes another function as an argument and returns a modified version of that function. Let’s start with a simple example:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
This code defines a decorator my_decorator
. The @my_decorator
syntax above say_hello
is syntactic sugar; it’s equivalent to:
= my_decorator(say_hello) say_hello
When say_hello()
is called, it first executes the code within wrapper()
, printing messages before and after the original say_hello()
function. This demonstrates the basic principle: the decorator wraps additional functionality around the original function.
Decorators with Arguments
The previous example showed a decorator without arguments. Let’s see how to handle decorators that need to accept arguments:
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
= func(*args, **kwargs)
result return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"Hello, {name}!")
"World") greet(
Here, repeat
is a decorator factory; it returns a decorator that repeats the decorated function a specified number of times. Note the use of *args
and **kwargs
in wrapper
to handle functions with various arguments.
Decorators with Return Values
Decorators can also handle functions that return values:
def bold_decorator(func):
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
@bold_decorator
def get_message():
return "Hello, World!"
print(get_message()) # Output: <b>Hello, World!</b>
This example shows how a decorator can modify the return value of the decorated function, adding HTML bold tags in this case.
Using functools.wraps
When using decorators, it’s important to preserve the metadata of the original function. The wraps
decorator from the functools
module helps with this:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper():
print("Before function call")
func()print("After function call")
return wrapper
@my_decorator
def say_hello():
"""This is a simple function."""
print("Hello!")
print(say_hello.__name__) # Output: say_hello (Preserves the name)
print(say_hello.__doc__) # Output: This is a simple function. (Preserves the docstring)
Without wraps
, the __name__
and __doc__
attributes would refer to the wrapper
function, not the original say_hello
function. wraps
ensures the original function’s metadata is preserved.
Practical Applications
Decorators are widely used in various scenarios, including:
- Logging: Record function calls and their arguments.
- Timing: Measure the execution time of functions.
- Authentication: Check user permissions before executing a function.
- Caching: Store the results of expensive function calls to improve performance.
- Input validation: Validate the input arguments of a function.
By mastering Python decorators, you can write more efficient, reusable, and elegant code. They offer a powerful mechanism to improve your functions without cluttering your codebase.