Decorators in Python are a powerful feature that allows us to modify the behavior of functions or classes. They allow us to wrap another function in order to extend the behavior of the wrapped function, without permanently modifying it. In this article, we will explore decorators and their usage for profiling and logging.
Table of Contents
- What is a Decorator?
- Use Cases of Decorators
- Python Decorator Example
- Python Decorators with Parameters
- Profiling with Decorators
- Logging with Decorators
- Conclusion
What is a Decorator?
A decorator is a callable that takes another function as an argument and returns yet another function. The returned function typically wraps the input function with some additional behavior. This allows us to easily add functionality to existing functions without modifying them directly.
Use Cases of Decorators
Decorators are used for a variety of tasks such as:
- Authentication: You can use a decorator to check if a user is authenticated before allowing them to access certain views in your web application.
- Logging: A decorator can be used to log function calls, timing information, and other useful debug information.
- Caching: Decorators can be used to cache the results of expensive function calls and return the cached result when the same inputs occur again.
- Validation: You can use a decorator to validate function arguments before they are passed to the decorated function.
- Modifying Function Behavior: A decorator can modify the behavior of a function without changing its source code, which makes it easier to maintain and understand your code.
Python Decorator Example
In Python, we use the @
symbol followed by the name of the decorator before a function definition to apply it to that function. For example:
def custom_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function is called, something happens.")
func(*args, **kwargs)
print("After the function is called, something happens.")
return wrapper
@custom_decorator
def say_hello(name):
print(f"Hello {name}")
say_hello("Developer")
In this example, say_hello
is decorated with custom_decorator
. When we call say_hello()
, it will first print "Before the function is called, something happens.", then call the original say_hello
function, and finally print "After the function is called, something happens."
Output:
Something is happening before the function is called.
Hello, world
Something is happening after the function is called.
Python Decorators with Parameters
In the following code example, a custom decorator named custom_decorator_with_params
is defined. This decorator accepts arguments, allowing for customization of its behavior. Within this decorator, there’s an inner function called inner, which acts as the actual decorator. Inside inner, another function named wrapper is defined. This wrapper function serves as a wrapper around the original function that will be decorated.
If the decorator specifies the upper_case
parameter and it’s set to True
, string arguments are transformed to uppercase before calling the original function.
The say_hello
function is then decorated using @custom_decorator_with_params(upper_case=True)
, which means that the upper_case
parameter is set to True
for this specific decoration. Finally, the decorated say_hello
function is invoked with the argument "Developer".
# Define a decorator function that accepts parameters
def custom_decorator_with_params(*dec_args, **dec_kwargs):
# Define an inner function that will be returned as the actual decorator
def inner(func):
# Define a wrapper function that will wrap the original function
def wrapper(*args, **kwargs):
# Check if the decorator has specified 'upper_case' parameter and it's True
if "upper_case" in dec_kwargs and dec_kwargs['upper_case'] == True:
# If True, transform any string arguments to uppercase
transformed_args = tuple(item.upper() if isinstance(item, str) else item for item in args)
# Call the original function with transformed arguments
func(*transformed_args, **kwargs)
else:
# If 'upper_case' parameter is not specified or it's False
func(*args, **kwargs)
return wrapper
return inner
@custom_decorator_with_params(upper_case=True)
def say_hello(name):
# Original function body
print(f"Hello {name}.")
# Call the decorated function
say_hello("Developer")
Output:
Hello DEVELOPER.
Profiling with Decorators
Profiling is a way to measure how long different parts of your code take to execute. Python provides a built-in module for this purpose: cProfile
. We can use decorators to easily add profiling to any function. Here’s an example:
import cProfile
def custom_profile_decorator(func):
def wrapper(*args, **kwargs):
profiler = cProfile.Profile()
result = profiler.runcall(func, *args, **kwargs)
profiler.print_stats()
return result
return wrapper
@custom_profile_decorator
def custom_function():
print("That's a test")
custom_function():
In this example, my_function
is decorated with profile_decorator
. When we call my_function()
, it will run the profiler and print out a report of how long each function call took.
Output:
3 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 test1.py:24(my_function)
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Logging with Decorators
Logging is another useful application for decorators. It allows us to keep track of what our program does without having to manually add logging statements throughout our code. Here’s an example:
import logging
logging.basicConfig(level=logging.INFO)
def custom_log_decorator(func):
def wrapper(*args, **kwargs):
logging.info(f"Calling the function: {func.__name__}")
result = func(*args, **kwargs)
logging.info(f"The function {func.__name__} returned: {result}")
return result
return wrapper
@custom_log_decorator
def custom_function():
print("That's a simple test")
custom_function()
In this example, custom_function
is decorated with custom_log_decorator
. When we call custom_function()
, it will log a message before and after the function call, including any arguments passed to the function.
Output:
INFO:root:Calling the function: custom_function
That's a simple test
INFO:root:The function custom_function returned: None
Conclusion
Decorators are a powerful tool in Python that allow us to easily add functionality to existing functions without modifying them directly. They can be used for profiling, logging, and many other purposes. By understanding how decorators work, you can write cleaner, more maintainable code.