Python Decorators With Arguments

Introduction

Python decorators are functions that allow you to enhance the functionality of other functions without modifying their original code directly. Decorators can accept arguments themselves, which can make them even more versatile. These arguments can modify the behavior of the decorator or the decorated function in various ways.

When defining a decorator with arguments, you essentially create a factory function for decorators. The factory function creates and returns decorator function. This allows you to customize the behavior of the decorator based on the arguments passed to it.

Understanding How Decorators With Arguments Work

Decorators with arguments are basically factory functions that create decorators. Consequently, the best approach to understanding this topic is to first understand decorators, then understand factory functions, and finally put them together. Knowledge of basic (with no arguments) decorators is a prerequisite for this lesson, and so we’ll start with a brief overview of factory functions.

Factory Functions

A factory function returns another function it creates based on arguments it receives. It can be conveniently implemented with a function that returns an inner function it defines.

Example:

def print_line_factory(length):
    def print_line():
        print('-' * length)
        
    return print_line
    
print_dash_40 = print_line_factory(40)
print_dash_40()

Output:

----------------------------------------

The example above creates a factory function, print_line_factory() (line 1), for printing horizontal dash (-) lines (line 1). It doesn’t print anything itself, but instead, returns the inner function print_line() (line 5). The inner function does the printing (line 3) based on the length argument of print_line_factory(). To print the line, the code calls the factory function with the value 40 (line 7). This call creates a function that prints a line 40 dashes long, which is assigned to the variable print_dash_40. Finally, the function print_dash_40() is called to print the line.

You can read more about inner and factory functions in the inner functions lesson.

Making a Decorator Factory

This section will build on the previous example to create a decorator factory. To start, let’s create a decorator that wraps the execution of any function with a line of dashes by printing it before and after.

Example:

def line_wrapper_decorator(fn):
    def decorated_function(*args, **kwargs):
        length = 40
        print('-' * length)
        return_value = fn(*args, **kwargs)
        print('-' * length)       
        return return_value
    
    return decorated_function
    
@line_wrapper_decorator
def say_hello():
    print("Hello")

say_hello()

Output:

----------------------------------------
Hello
----------------------------------------

The example above creates a decorator by defining an inner function (line 2) and returning it (line 9). The inner function prints dashed lines (line 4 and 6) around the execution of the original function (line 5). The code uses the decorator to decorate the say_hello() function using the @ syntax (line 11) to produce the word “Hello” surrounded by lines, 40 dashes long.

Now, let’s make the length of the lines configurable by putting the decorator in a factory function that accepts the length as an argument.

Example:

def line_wrapper_decorator_factory(length):
    def line_wrapper_decorator(fn):
        def decorated_function(*args, **kwargs):
            print('-' * length)
            return_value = fn(*args, **kwargs)
            print('-' * length)       
            return return_value
        
        return decorated_function
     
    return line_wrapper_decorator
    
@line_wrapper_decorator_factory(10)
def say_hello():
    print("Hello")

say_hello()

Output:

----------
Hello
----------

The example above may seem complicated because there are three nested functions. Luckily, all we’ve done is put a decorator function inside a factory function. And by now, you should have a good grasp of both. The line_wrapper_decorator() decorator function (lines 2-9) is the inner function that the line_wrapper_decorator_factory() factory returns (line 11).

The line_wrapper_decorator() function is almost identical to the version in the previous example, except that instead of length being a local variable, it is a parameter of line_wrapper_decorator_factory().

The function say_hello() is decorated with @line_wrapper_decorator_factory(10) (line 13). Despite the parentheses and argument, there is no new syntax here. The @ syntax simply decorates the function that follows with the decorator function it’s given, and a call to line_wrapper_decorator_factory(10) happens to return a decorator function, which is the 10-dash version of line_wrapper_decorator().

Code Sample for a General Decorator With Arguments

Here is sample code that demonstrates how to create a general decorator with arguments:

def decorator_factory_function(arg)
    def decorator_function(original_function):
        def modified_function(*args, **kwargs):
            # Add functionality here, possibly involving the argument `arg`
            original_function(*args, **kwargs)  # Use the original function
            # Add functionality here, possibly involving the argument `arg`
            
        return modified_function
        
     return decorator_function

These are the key elements of the code above:

  • decorator_factory_function: A function that creates a decorator function with an argument arg.
  • arg: An argument that can be passed to the decorator factory function to customize the behavior of the decorator. (You can have more than one argument, since a factory is just a regular Python function.)
  • decorator_function: The decorator function returned by decorator_factory_function. It takes the original function as its argument.
  • original_function: The function to be modified, i.e. decorated.
  • modified_function: The modified version of the original function. It calls the original function and uses the factory function arguments.
  • *args, **kwargs: A generic parameter declaration that enables modified_function to enhance a function that has any combination of parameters.

Note that even though a decorator with arguments is often thought of as a decorator, it is technically a factory function that returns a decorator. It only becomes a decorator after the factory function is called.

To use a decorator with arguments, simply call it with its argument(s), e.g. decorator_factory_function(some_value), and treat the returned decorator just as you would any other decorator.

Here is sample code that shows how to use a decorator with arguments using the @ syntax:

@decorator_factory_function(arg)
def my_function():
    ...

Summary & Reference for Python Decorators With Arguments

To define a decorator with arguments, you create a factory function that returns a decorator function. The factory function can have one or more arguments and creates the decorator function based on those.

def decorator_factory_function(arg)  # factory function
    def decorator_function(original_function):  # decorator function
        def modified_function(*args, **kwargs):  # inner function that does the 'decorating'
           ...  # code that calls 'original_function' and uses 'arg' goes here
            
        return modified_function  #tThe decorator returns the decorated function here
        
     return decorator_function  # the factory returns the decorator here

To use a decorator with arguments, just call the factory function, passing it the arguments. Since the factory function call returns a decorator function, it can be used anywhere a decorator function can.

@decorator_factory_function(arg)
def my_function():
    ...