Creating A Decorator Factory in Python

This article will go over creating a decorator factory in Python. This can be helpful for example when you want to create different logging capabilities. The final result of this article is:

def decorator(declared_decorator):
    """Creates a decorator which will be used as a wrapper."""

    @functools.wraps(declared_decorator)
    def final_decorator(func=None, **kwargs):
        def decorated(func):
            @functools.wraps(func)
            def wrapper(*a, **kw):
                return declared_decorator(func, a, kw, **kwargs)
            return wrapper

        if func is None:
            return decorated
        else:
            return decorated(func)
    return final_decorator

Decorators

Let’s go over a brief overview of decorators in Python. Decorators allow you to have a common set of tasks in one area such as checking for required arguments before moving forward. Other examples of using decorators are for caching, logging, error handling, and access control. If we wanted to create a logging decorator we can do this:

def logging(func):
    """Log when a function is called."""
    def log_call(*args, **kwargs): 
        print('calling function {}'.format(func.__name__))
        return func(*args, **kwargs)
    return log_call

When then implement this decorator on a function:

@logging
def my_func():
    pass

Using the `@logging` syntax is the same as `my_func = logging(my_func)` which actually returns the inner `log_call` function. The `func` passed into the decorator function is saved in the scope (via closure) and is possible due to functions being treated as first class objects (they can be passed in, assigned to variables, can return a function from a function). When we now call `my_func` it is actually calling a reference to `log_call` which logs the function call then calls the original reference to `my_func` saved in the decorator scope.

Since `log_call` is actually the function being called we lost some attributes inherit to the `my_func` function such as it’s name, docstring, and argument list. If you were to try to get this data from the new `my_func` function after being decorated, you’d be actually getting this data from ther `log_call` function. To fix this, we need to use the `functools.wraps` decorator:

import functools

def logging(func):
    """Log when a function is called."""

    @functools.wraps(func)
    def log_call(*args, **kwargs): 
        print('calling function {}'.format(func.__name__))
        return func(*args, **kwargs)
    return log_call

Decorators With Arguments

Sometimes we want to pass arguments into a decorator. I recently had to do this for checking required arguments for a REST application. In my web application, all functions return a dict with status (boolean) and data properties. The final implementation to check for arguments is here:

def verify_parameters(required):
    """Verifies passed in arguments."""
    required = required.split(' ')
    def decorator(function):
        def wrapper(**kwargs):
            missing_params = missing_parameters(params=kwargs, required=required)
            if missing_params:
                return {"data": f"${function.__name__} is missing required parameters: {missing_params}", "status": False}
            else:
                return function(**kwargs)
        return wrapper
    return decorator


def missing_parameters(params=None, required=None):
    """Checks for missing parameters."""
    params = [] if params is None else params
    required = [] if required is None else required

    # get any missing required keys
    missing_keys = [x for x in required if not params.get(x)]
    if missing_keys:
        return 'Missing the following required args: ' + ', '.join(missing_keys)

To use it, I ‘call’ the decorator function:

@verify_parameters('cred_hash msrp key')
def transition_to_pcr(**kwargs):
    pass

In order to use decorator arguments, we actually need three nested functions: the function we call when passing in arguments, the decorator function itself, and the original function. When we call `@verify_parameters(‘cred_hash msrp key’)` we invoke the `verify_parameters` which saves a copy of the required parameters. It will then return the `decorator` function as if it’s a regular decorator. From there, the `@` will use the the regular decorator to wrap the original function like we did in the first example. The `wrapper` function is what will be returned and used in the regular flow of the web application. `functools.wraps` was not included her as it was not needed.

Decorators With or Without Arguments

So how does this tie into or decorator factory example? Given a general decorator that passes in arguments we get:

def final_decorator(func=None, **kwargs):
    def decorated(func):
        @functools.wraps(func)
        def wrapper(*a, **kw):
            return func(*a, **kw)
        return wrapper
    return decorated

This doesn’t do anything so it’s pretty useless but what if we wanted it to be optional to pass in arguments to the decorator? We would need to add a check if anything was passed in. One thing to note is this requires you to pass arguments as keyword arguments.

def final_decorator(func=None, **kwargs):
    def decorated(func):
        @functools.wraps(func)
        def wrapper(*a, **kw):
            return func(*a, **kw)
        return wrapper

    # if args passed then don't decorate function just yet
    # else decorate function before passing it back
    if func is None:
        return decorated
    else:
        return decorated(func)

Decorator Factory

Finally, we can create a decorator factory. Instead of using all the boilerplate above to create decorators with (or without) arguments, we can create a decorator of decorators.

def decorator(declared_decorator):
    """Creates a decorator which will be used as a wrapper."""

    @functools.wraps(declared_decorator)
    def final_decorator(func=None, **kwargs):
        """This is the real decorator that will be used 
        in your application."""
        def decorated(func):
            """This is the decorator function that will 
            be called when setting a decorator."""
            @functools.wraps(func)
            def wrapper(*a, **kw):
                """This is the new function that will be 
                returned to use in your application."""
                return declared_decorator(func, a, kw, **kwargs)
            return wrapper

        # if decorator called with arguments
        if func is None:
            return decorated
        else:
            # if called without arguments 
            # we should decorate immediately
            return decorated(func)
    return final_decorator

There are three required arguments:

  • The function that will be decorated
  • A tuple of positional arguments
  • A dictionary of keyword arguments

Decorator Factory Example

Let’s use this decorator factory to create different type of decorators. If we want to create a decorator that can accept different ways to log that we calling the function:

@decorator
def logger_decorator(func, args, kwargs, log_func=None):
    log_func('calling function '.format(func.__name__))
    return func(*args, **kwargs)

From there, we can create two decorators to log to console like we did before or to the `logging` library:

# logs to `print` when called
@logger_decorator(log_func=print)
def my_func():
    pass

# logs to `logger.info` when called
import logger
@logger_decorator(log_func=logger.info)
def my_func_again():
    pass

Conclusion

And that’s it! We went over using decorators with and without arguments. Creating a decorator that accepts both, and finally a decorator of decorators. With the decorator factory, we can easily generate all types of decorator factories such as logging when being called, then from there create all types of decorators that will log in different ways. Decorators helped me check required arguments for a web application can be used for so much more. If you find yourself doing the same thing for multiple functions, a decorator is sure way to clean up that code duplication.

One Comment on “Creating A Decorator Factory in Python”

Leave a Reply

Your email address will not be published.