How To Create Custom Decorators In Python

Click to share! ⬇️

Python decorators are a powerful feature that allows developers to modify the behavior of functions and methods. They are essentially a way to add functionality to existing code without modifying the original code. A decorator is a function that takes another function as an argument and returns a new function that includes the additional functionality. The syntax for applying a decorator to a function is to use the “@” symbol followed by the name of the decorator, placed on the line before the function definition.

For example, consider the following code:

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("hello!")

say_hello()

The my_decorator function takes in a function func and returns a new function wrapper that includes additional functionality. In this case, the wrapper function will print “Something is happening before the function is called.” before calling the original func and “Something is happening after the function is called.” after the call.

When the say_hello function is defined, it is decorated with the my_decorator function using the “@” symbol. When the say_hello function is called, the additional functionality in the wrapper function is executed first, then the original say_hello function is executed.

Decorators do not modify the original function; instead, they return a new function that includes the additional functionality. This allows for easy and efficient code reuse.

Python decorators are a powerful feature that allows developers to add functionality to existing code without modifying it. They are defined as functions that take another function as an argument and return a new function that includes additional functionality. They are applied to functions or methods using the “@” symbol.

Defining A Simple Custom Decorator

Now that you have a basic understanding of how Python decorators work, let’s dive deeper and learn how to create your own custom decorator.

A simple custom decorator can be created by defining a function that takes in another function as an argument, and returns a new function that includes the additional functionality. The new function can be created by defining a nested function within the decorator function.

Here is an example of a simple custom decorator that logs the execution time of a function:

import time

def log_execution_time(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Executed in {end_time - start_time} seconds.")
        return result
    return wrapper

@log_execution_time
def my_function():
    print("Doing some work.")
    time.sleep(2)

my_function()

In this example, the log_execution_time decorator takes in a function func and returns a new function wrapper that includes the additional functionality. The wrapper function starts a timer, calls the original function func, stops the timer, and calculates the execution time. It then print the execution time and return the result of the original function.

When the my_function is defined, it is decorated with the log_execution_time decorator using the “@” symbol. When the my_function is called, the wrapper function is executed, logging the execution time.

It is important to note that the wrapper function takes in arbitrary arguments (*args) and keyword arguments (**kwargs) so that it can be applied to any function regardless of its arguments.

Passing Arguments To A Custom Decorator

In some cases, you may need to pass additional arguments to your custom decorator. This can be achieved by defining the decorator function to take in the desired arguments, in addition to the function it is decorating.

Here is an example of a custom decorator that takes in a threshold argument and only logs the execution time if it exceeds the threshold:

import time

def log_execution_time(threshold):
    def timing_decorator(func):
        def wrapper(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            execution_time = end_time - start_time
            if execution_time > threshold:
                print(f"Executed in {execution_time} seconds.")
            return result
        return wrapper
    return timing_decorator

@log_execution_time(threshold=1)
def my_function():
    print("Doing some work.")
    time.sleep(2)

my_function()

In this example, the log_execution_time function takes in the threshold argument and returns a new function timing_decorator. The timing_decorator takes in a function func and returns a new function wrapper that includes the additional functionality. The wrapper function starts a timer, calls the original function func, stops the timer, and calculates the execution time. If the execution time exceeds the threshold, it print the execution time, otherwise it does nothing and return the result of the original function.

When the my_function is defined, it is decorated with the log_execution_time decorator using the “@” symbol, and passing the threshold value of 1 second. When the my_function is called, the wrapper function is executed, and it logs the execution time if it exceeds the threshold.

When passing an argument to a decorator, you will need to use parentheses to call the decorator function with the argument, and then use the “@” symbol to decorate the function.

Passing arguments to a custom decorator is easy. You need to define the decorator function to take in the desired arguments, in addition to the function it is decorating. The inner function that does the actual decoration can access those argument and use them to perform the desired functionality.

Using Classes As Decorators

In addition to functions, Python classes can also be used as decorators. This can be useful when you need to maintain state or encapsulate more complex logic.

Here is an example of a class-based decorator that logs the execution time of a function, and also keeps track of the number of times the function has been called:

import time

class LogExecutionTime:
    def __init__(self):
        self.counter = 0

    def __call__(self, func):
        def wrapper(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            execution_time = end_time - start_time
            self.counter += 1
            print(f"Executed {func.__name__} {self.counter} times in {execution_time} seconds.")
            return result
        return wrapper

log_time = LogExecutionTime()

@log_time
def my_function():
    print("Doing some work.")
    time.sleep(2)

my_function()
my_function()

In this example, the LogExecutionTime class has a __call__ method which is called when the class instance is used as a decorator. The __call__ method takes in a function and returns a new function wrapper that includes the additional functionality. The wrapper function starts a timer, calls the original function, stops the timer, and calculates the execution time. It then increments the counter attribute of the class, which keeps track of how many times the function has been called, and print the execution time and the number of times the function has been called.

When the my_function is defined, an instance of the LogExecutionTime class is created and assigned to the variable log_time, then log_time is decorated my_function using the “@” symbol. When the my_function is called, the wrapper function is executed, logging the execution time and incrementing the counter.

When using classes as decorators, the class must have a __call__ method, which is called when the class instance is used as a decorator.

Using classes as decorators can be useful when you need to maintain state or encapsulate more complex logic. To use a class as a decorator, the class must have a __call__ method, which is called when the class instance is used as a decorator. The __call__ method should take in a function and return a new function that includes the additional functionality, just like a regular decorator function.

Decorating Methods And Classes

Here is an example of decorating a method:

class MyClass:
    def __init__(self):
        self.counter = 0

    @classmethod
    def my_method(cls):
        print("My method has been called.")
        cls.counter += 1

obj = MyClass()
obj.my_method()
obj.my_method()
print(obj.counter)

In this example, the my_method method is decorated with @classmethod which is a built-in Python decorator. This decorator modifies the method so that it is bound to the class and not the instance. This means that when the method is called, it receives the class as its first argument instead of the instance. The class is passed as the cls argument, which is used to access the class variable counter and increment it by 1 each time the method is called.

Here is an example of decorating a class:

def my_decorator(cls):
    class Wrapper:
        def __init__(self, *args, **kwargs):
            self.wrapped = cls(*args, **kwargs)
        def __getattr__(self, name):
            return getattr(self.wrapped, name)
    return Wrapper

@my_decorator
class MyClass:
    def __init__(self):
        self.counter = 0
    def my_method(self):
        print("My method has been called.")
        self.counter += 1

obj = MyClass()
obj.my_method()
obj.my_method()
print(obj.counter)

In this example, a decorator my_decorator is defined, which takes in a class and returns a new class Wrapper that wraps the original class. The Wrapper class has an __init__ method that takes the same arguments as the original class and creates an instance of the original class, which is stored as an attribute wrapped. The Wrapper class also has a __getattr__ method which is used to forward attribute access to the wrapped class. This allows the Wrapper class to act as a transparent wrapper around the original class.

When the MyClass is defined, it is decorated with the my_decorator using the “@” symbol. When an instance of MyClass is created, an instance of the Wrapper class is created instead, which wraps the original class and allows it to be decorated.

Decorators can be applied to methods and classes in the same way as they are applied to functions. When decorating a method, the method is typically decorated with a built-in Python decorator like @classmethod or @staticmethod, which modifies the method’s behavior. When decorating a class, a custom decorator function is typically defined and applied using the “@” symbol. The custom decorator function should take in a class and return a new class that wraps the original class and provides the desired functionality.

Decorator Chaining

Python decorators can be applied to a single function or method multiple times, creating a chain of decorators. This is known as decorator chaining.

Here is an example of chaining two decorators together:

def decorator_1(func):
    def wrapper(*args, **kwargs):
        print("Decorator 1")
        return func(*args, **kwargs)
    return wrapper

def decorator_2(func):
    def wrapper(*args, **kwargs):
        print("Decorator 2")
        return func(*args, **kwargs)
    return wrapper

@decorator_1
@decorator_2
def my_function():
    print("My function")

my_function()

In this example, the my_function is decorated with both decorator_1 and decorator_2 using the “@” symbol. When my_function is called, it first executes decorator_1, then decorator_2, and finally the function itself.

When chaining decorators, the order of the decorators is important as the decorators are applied in the reverse order they are listed. So in this example, decorator_1 is applied first and decorator_2 is applied second. This means that decorator_1 will run first and decorator_2 will run second.

It is also possible to chain decorators that accept arguments. This is done by passing the argument to the decorator when it is applied.

def decorator_1(arg):
    def inner_decorator(func):
        def wrapper(*args, **kwargs):
            print("Decorator 1: arg =", arg)
            return func(*args, **kwargs)
        return wrapper
    return inner_decorator

@decorator_1(arg="some value")
def my_function():
    print("My function")

my_function()

In this example, decorator_1 is defined to accept an argument arg and returns an inner decorator function that takes in a function and applies the decorator.

When chaining multiple decorators together, it is important to understand the order in which the decorators are applied and how they modify the behavior of the function or method. Careful consideration should be given to the order of the decorators and how they interact with each other to ensure that the desired behavior is achieved.

Custom Decorators FAQ

  1. What are Python decorators?

Python decorators are a way to modify the behavior of a function or method by wrapping it in another function or class. They are often used to add functionality such as logging, timing, or caching to a function or method.

  1. How do I create a custom decorator in Python?

To create a custom decorator, you need to define a function or class that takes in a function or method and returns a new function or method that wraps the original. The wrapper function or method can then modify the behavior of the original function or method before or after it is called.

  1. Can I pass arguments to a custom decorator?

Yes, it is possible to pass arguments to a custom decorator. This is done by defining the decorator function to accept arguments and then passing those arguments when the decorator is applied to a function or method.

  1. What is decorator chaining?

Decorator chaining is the process of applying multiple decorators to a single function or method. The decorators are applied in the reverse order they are listed, so the first decorator listed will be applied last.

  1. How do I chain custom decorators together?

To chain custom decorators together, simply list them before the function or method they are decorating, separated by the “@” symbol. The decorators will be applied in the reverse order they are listed.

  1. How can I ensure that my custom decorators work correctly when chained together?

When chaining custom decorators together, it is important to understand the order in which they are applied and how they modify the behavior of the function or method. Careful consideration should be given to the order of the decorators and how they interact with each other to ensure that the desired behavior is achieved.

  1. Are there any best practices for creating custom decorators?

Some best practices for creating custom decorators include keeping the decorator logic simple and easy to understand, being mindful of the order in which decorators are applied, and testing the decorator thoroughly to ensure it behaves as expected.

Click to share! ⬇️