Decorators are a powerful feature in Python that allow you to modify or enhance the behavior of functions and classes. In this chapter, we will explore how decorators can be utilized for logging, authorization, and caching purposes. We'll start from the basics and gradually dive into more advanced concepts, providing detailed examples along the way.
In this section, we’ll explore how decorators can be used for logging to capture and record information about function calls.
Let’s start with a basic example of a logging decorator that logs information about function calls.
def log_function(func):
def wrapper(*args, **kwargs):
print(f"Calling function {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} returned: {result}")
return result
return wrapper
@log_function
def add(a, b):
return a + b
print(add(3, 5))
Calling function add with args: (3, 5), kwargs: {}
Function add returned: 8
8
log_function
decorator logs information about the function add
before and after its execution. When add(3, 5)
is called, the decorator prints information about the function call and its return value.You can enhance the logging decorator to include custom messages and additional information.
def log_with_message(message):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"{message}: Calling function {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"{message}: Function {func.__name__} returned: {result}")
return result
return wrapper
return decorator
@log_with_message("INFO")
def multiply(a, b):
return a * b
print(multiply(3, 5))
INFO: Calling function multiply with args: (3, 5), kwargs: {}
INFO: Function multiply returned: 15
15
log_with_message
decorator takes a message as an argument and prints it along with the logging information. This allows you to customize the logging output based on different contexts or levels of importance.In this section, we’ll explore how decorators can be used for implementing authorization logic to control access to functions or resources.
Let’s start with a basic example of an authorization decorator that restricts access to a function based on user roles.
def authorize(allowed_roles):
def decorator(func):
def wrapper(user_role, *args, **kwargs):
if user_role in allowed_roles:
return func(*args, **kwargs)
else:
raise PermissionError("Unauthorized access")
return wrapper
return decorator
@authorize(["admin", "manager"])
def delete_user(user_id):
print(f"User {user_id} deleted")
delete_user("admin", user_id=123)
User 123 deleted
authorize
decorator restricts access to the delete_user
function based on the user’s role. Only users with roles “admin” or “manager” are authorized to delete users.You can enhance the authorization decorator to include user authentication logic.
def authenticate(func):
def wrapper(user, *args, **kwargs):
if user.is_authenticated:
return func(user, *args, **kwargs)
else:
raise PermissionError("User not authenticated")
return wrapper
@authenticate
@authorize(["admin"])
def change_password(user, new_password):
user.password = new_password
print("Password changed successfully")
class User:
def __init__(self, username, password):
self.username = username
self.password = password
self.is_authenticated = True
admin_user = User("admin", "password123")
change_password(admin_user, "newpassword123")
Password changed successfully
authenticate
decorator checks if the user is authenticated before allowing access to the change_password
function. The user object passed to the function contains authentication information such as the username and password.In this section, we’ll explore how decorators can be used for caching to optimize the performance of functions by storing previously computed results.
Let’s start with a basic example of a caching decorator that stores the results of function calls in a dictionary.
def cache(func):
cached_results = {}
def wrapper(*args):
if args in cached_results:
print("Retrieving result from cache")
return cached_results[args]
else:
result = func(*args)
cached_results[args] = result
return result
return wrapper
@cache
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(5))
print(fibonacci(5))
Retrieving result from cache
5
Retrieving result from cache
5
cache
decorator caches the results of function calls to the fibonacci
function. When the same input is provided to the function again, the cached result is retrieved from the cache instead of recomputing the result.You can enhance the caching decorator to include time-based expiry of cached results.
import time
def cache_with_expiry(expiry_time):
cached_results = {}
def decorator(func):
def wrapper(*args):
if args in cached_results:
result, timestamp = cached_results[args]
if time.time() - timestamp <= expiry_time:
print("Retrieving result from cache")
return result
result = func(*args)
cached_results[args] = (result, time.time())
return result
return wrapper
return decorator
@cache_with_expiry(expiry_time=60)
def square(x):
return x ** 2
print(square(5))
time.sleep(30)
print(square(5))
time.sleep(40)
print(square(5))
25
Retrieving result from cache
25
36
cache_with_expiry
decorator caches the results of function calls with a time-based expiry. The cached results are stored with a timestamp, and if the cached result is older than the expiry time, the function result is recomputed.In this comprehensive exploration, we explored the versatile capabilities of decorators in Python for logging, authorization, and caching. Decorators provide a concise and powerful way to modify the behavior of functions and classes without altering their source code directly. We started by understanding the basics of decorators and then delved into more advanced concepts such as customizing decorator behavior, chaining decorators, and parameterized decorators. Happy Coding!❤️