Implementing Custom Decorator Functions in Python (55/100 Days of Python)

Martin Mirakyan
4 min readFeb 25, 2023

--

Day 55 of the “100 Days of Python” blog post series covering the basics of how to create custom decorators

In Python, a decorator is a special type of function that can be used to modify the behavior of another function. Decorators allow you to add functionality to an existing function without modifying its code directly. This can be useful for adding logging, authentication, or other types of functionality to a function without cluttering up its code.

Basics of Decorator Functions

A decorator function in Python is a higher-order function that takes a function as an argument and returns a new function. The new function can be used to modify the behavior of the original function. We can define a decorator function as follows:

def my_decorator(original_function):
def new_function():
# do something before the original function
original_function()
# do something after the original function
return new_function

To use a decorator function, you can apply it to an existing function by placing the decorator function name above the function definition:

@my_decorator
def original_function():
print('Hello, World!')

Now, when you call the original function, the decorator function will be executed before and after the original function:

original_function()
# do something before the original function
# Hello, World! -> the original function
# do something after the original function

Real-World Use Cases of Decorator Functions

Decorators are a powerful feature in Python and can be used for a variety of tasks. Some of the most common use cases for decorators include:

  1. Logging: You can use a decorator to log information about function calls, such as the arguments passed to the function and the return value.
  2. Caching: You can use a decorator to cache the results of a function, so that if the function is called again with the same arguments, the cached result is returned instead of re-computing the result.
  3. Authentication: You can use a decorator to require authentication before a function can be called, ensuring that only authorized users have access to the function.
  4. Timing: You can use a decorator to measure the time it takes for a function to execute, which can be useful for optimizing performance.

Creating a Custom Decorator in Python

Creating a custom decorator in Python is easy. You can define a function that takes a function as an argument and returns a new function that modifies the behavior of the original function. Here’s an example of a custom decorator that logs information about a function call:

import logging


def log_function_call(original_function):
def new_function(*args, **kwargs):
logging.info(f'Calling function {original_function.__name__} with args={args} kwargs={kwargs}')
result = original_function(*args, **kwargs)
logging.info(f'Function {original_function.__name__} returned {result}')
return result

return new_function

In this example, the log_function_call decorator takes a function as an argument and returns a new function that logs information about the function call. The *args and **kwargs parameters allow the decorator to accept any number of positional and keyword arguments. To use this decorator, you simply apply it to an existing function:

@log_function_call
def my_function(x, y):
return x + y

Now, when you call the my_function function, the log_function_call decorator will log information about the function call:

my_function(1, 2)
# Calling function my_function with args=(1, 2) kwargs={}
# Function my_function returned 3

Decorators With Arguments

In Python, decorators can also accept arguments. These types of decorators are called “decorator factories” because they return a decorator function that can be used to modify the behavior of another function with specific arguments.

To create a decorator factory, you define a function that takes arguments and returns a decorator function. The decorator function can then be used to modify the behavior of another function. Here’s an example of a decorator factory that takes an argument n and returns a decorator function that multiplies the result of the decorated function by n:

def multiply_result_by(n):
def decorator(original_function):
def new_function(*args, **kwargs):
result = original_function(*args, **kwargs)
return result * n
return new_function
return decorator

In this example, multiply_result_by is a decorator factory that takes an argument n. The returned decorator function, decorator, takes a function as an argument and returns a new function that multiplies the result of the decorated function by n.

To use this decorator factory, you can apply it to a function and pass the argument n:

@multiply_result_by(2)
def my_function(x, y):
return x + y

Now, when you call the my_function function, the result is multiplied by 2:

my_function(1, 2)  # 6

You can also use multiple decorator factories to apply multiple decorators to a function with different arguments:

@multiply_result_by(2)
@log_function_call
def my_function(x, y):
return x + y

In this example, the multiply_result_by decorator factory multiplies the result of my_function by 2, and the log_function_call decorator logs information about the function call.

Notice how the function multiply_result_by returns a decorator. Which then wraps the function passed to it through @ and extends the functionality.

It’s also important to keep in mind that the order of the decorators can matter in some cases. For instance, in the example above, if we swap the places of @multiply_result_by and @log_function_call, we would obtain different results:

@multiply_result_by(2)
@log_function_call
def my_function(x, y):
return x + y


my_function(1, 2)
# Calling function my_function with args=(1, 2) kwargs={}
# Function my_function returned 3
@log_function_call
@multiply_result_by(2)
def my_function(x, y):
return x + y

my_function(1, 2)
# Calling function my_function with args=(1, 2) kwargs={}
# Function my_function returned 6

This happens because Python executes the functions in the given order. So, if we first decorate our function with @multiply_result_by, and then @log_function_call, it will execute the function, then log the result, and then multiply the result. Yet, if we swap the order of logging and multiplying, then Python will first execute the function, then multiply the resulting number, and then log the result.

What’s next?

--

--