Using Decorators for Logging, Authorization, and Caching

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.

Logging with Decorators

In this section, we’ll explore how decorators can be used for logging to capture and record information about function calls.

Basic Logging Decorator

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))
				
			

Output:

				
					Calling function add with args: (3, 5), kwargs: {}
Function add returned: 8
8
				
			

Explanation:

  • In this example, the 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.

Logging with Custom Messages

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))
				
			

Output:

				
					INFO: Calling function multiply with args: (3, 5), kwargs: {}
INFO: Function multiply returned: 15
15
				
			

Explanation:

  • In this example, the 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.

Authorization with Decorators

In this section, we’ll explore how decorators can be used for implementing authorization logic to control access to functions or resources.

Basic Authorization Decorator

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)
				
			

Output:

				
					User 123 deleted
				
			

Explanation:

  • In this example, the 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.

Authorization with User Authentication

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")
				
			

Output:

				
					Password changed successfully
				
			

Explanation:

  • In this example, the 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.

Caching with Decorators

In this section, we’ll explore how decorators can be used for caching to optimize the performance of functions by storing previously computed results.

Basic Caching Decorator

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))
				
			

Output:

				
					Retrieving result from cache
5
Retrieving result from cache
5
				
			

Explanation:

  • In this example, the 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.

Decorator with Time-based Expiry

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))
				
			

Output:

				
					25
Retrieving result from cache
25
36
				
			

Explanation:

  • In this example, the 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!❤️

Table of Contents