Writing Your Own Function Decorators in Python

Motivation

Function decorators are a powerful feature in Python that allows modifying the behavior of a function without changing its source code. I have always wondered how they work and how to write my own. In this article, I show you how to write your own decorators in python. I will use authorization and caching as an example.

Use Cases

There are numerous reasons why you might want to modify the behavior of a function with a decorator. Here are a few:

  • Logging: Decorators can be used to add logging to a function. By wrapping a function with a logging decorator, you can log the function call details such as the time of the call, the arguments passed, and the returned value.

  • Authentication and Authorization: Decorators can be used to add authentication and authorization to a function. By adding authentication and authorization decorators, you can restrict access to a function only to authorized users.

  • Caching: You can use decorators to add caching to a function. This is useful when you have a function that takes a long time to execute, but returns the same result for the same input.

  • Validation: Decorators can be used to validate the input parameters of a function. By adding validation decorators, you can ensure that the input parameters are of the correct type and have the correct values.

  • Error Handling: Decorators can be used to handle exceptions raised by a function. By adding an error handling decorator, you can catch and handle exceptions raised by a function in a consistent way.

Caching

Sometimes, you want to prevent a function that performs laborious calculations to repeat work it has already done if the input is the same.

Here is how you can set up a decorator.

def cache_results(func):
    cached_results = {}

    def wrapper(*args):
        if args in cached_results:
            return cached_results[args]
        result = func(*args)
        cached_results[args] = result
        return result

    return wrapper

@cache_results
def fetch_data_from_database(url):
    print('Fetching data from database...')
    time.sleep(3)
    print('Done fetching data from database...')
    return 'Here is the sample data'

print(fetch_data_from_database('http://www.google.com'))
print(fetch_data_from_database('http://www.google.com'))

In this example, we define a decorator function cache_results that takes a function func as an argument and returns a new function wrapper. The wrapper function first checks if the arguments passed to func are in a dictionary cached_results, which serves as a cache for the function. If the result is already in the cache, the function returns the cached result. If the result is not in the cache, the function calls the original function func with the same arguments, stores the result in cached_results, and returns the result.

To use the cache_results decorator, simply apply it to the function that we want to cache by putting the decorator name (@cache_results) above the function definition. In this example, I decorate the fetch_data_from_database() function with the cache decorator, which means that the function will only be called once for each unique argument, and subsequent calls with the same argument will return the cached result.

When we call the fetch_data_from_database() function multiple times with the same argument (in this case, 'http://www.google.com'), you can see that the function is only called once, and subsequent calls return the cached result. In other words, there is no three second delay when the function is called a second time - the cached results are returned immediately.

Authorization

Sometimes, you might want to execute a function only if the user has the proper authorization. For example, you might only want to allow them to access parts of the database if they are a superuser. Here is how you can set up a decorator to run a function only if the user is a superuser. Otherwise, an exception is raised. (I created a RegularUser and a SuperUser class just as an example.)

def requires_super_user(func):
    def wrapper(user):
        if isinstance(user, SuperUser):
            return func(user)
        else:
            raise Exception('Forbidden: user is not a superuser')
    return wrapper

@requires_super_user
def access_database(user):
    print('Accessing database...')


def decorator_test():
    super_user = SuperUser('super_user')
    regular_user = RegularUser('regular_user')
    try:
        access_database(regular_user)
    except Exception as e:
        print(e)


In this example, we define a decorator function called requires_super_user that takes a function as an argument and returns a new function wrapper. The wrapper function first checks if the user is a superuser by checking if it is an instance of the SuperUser class. If so, it calls the original function passed as an argument (func) with the same arguments and returns its result. If the user is not an instance of SuperUser, it raises an exception with a message indicating that access is forbidden because the user is not a superuser.

To use the requires_auth decorator, we simply apply it to the function that requires authorization by putting the decorator name (@requires_super_user) above the function definition. In the example above, we decorate the decorator_test with the @requires_super_user decorator, which means that it can only be called if the user is a superuser.

That is all there is to it! Give it a try. I thought decorators were really difficult and they are not bad at all.

PythonDecoratorsFunctions
Avatar for tony-albanese

Written by tony-albanese

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.