Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Python Decorators: Building Custom Functionality

July 30, 2024

Python Decorators: Building Custom Functionality

In Python, decorators are a powerful feature that allows you to enhance the functionality of functions or classes. Decorators provide a way to modify or extend code without permanently modifying the original implementation. They are a great tool for implementing cross-cutting concerns, such as logging, caching, and validation, in a modular and reusable way.

What are Decorators?

Decorators are functions that modify other functions or classes at the time of declaration. They take a target function as an argument and return a new function(s) that replaces or wraps the original function. This enables you to add additional behavior before, after, or around the target function without changing its source code.

Creating Decorators

To create a decorator, you define a function that takes a function as an argument, decorates it, and returns a new function. The inner function, which performs the decoration, can access and modify the target function’s behavior.

Let’s start with a simple example that adds logging capabilities to a function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} completed")
        return result
    return wrapper

@log_decorator
def greet(name):
    return f"Hello, {name}!"

greet("Alice")

The log_decorator function is our decorator. It takes a function and defines an inner function called wrapper, which performs the logging. Finally, it returns the wrapper.

The @log_decorator syntax is used to apply the decorator to the greet function. When we call the greet function, it will execute the wrapper function, which adds the logging functionality before and after calling the original function. The output will be:

1
2
3
Calling function: greet
Function greet completed
Hello, Alice!

Decorating Functions with Arguments

Decorators can be used with functions that have arguments as well. To make our log_decorator work on functions with arguments, we need to use *args and **kwargs in the wrapper function signature. This allows the decorator to accept any number of positional or keyword arguments.

1
2
3
4
5
@log_decorator
def add(a, b):
    return a + b

add(2, 3)

The output will be:

1
2
3
Calling function: add
Function add completed
5

Chaining Multiple Decorators

You can apply multiple decorators to a function by stacking them on top of each other using the @ syntax. The order of the decorators is important, as they are applied from bottom to top.

1
2
3
4
@decorator1
@decorator2
def my_function():
    pass

In the above example, my_function will first be decorated by decorator2, and then the resulting function will be decorated by decorator1.

Creating Decorators with Parameters

Sometimes, you may want to pass arguments to your decorators, such as configuration options. To achieve this, we can define a decorator factory, which is a higher-order function that returns a decorator based on the given parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def repeat(count=3):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(count):
                print(func(*args, **kwargs))
        return wrapper
    return decorator

@repeat(count=2)
def hello(name):
    return f"Hello, {name}!"

hello("Bob")

In this example, we define a decorator factory called repeat that takes a parameter count. It returns a decorator, which repeats the execution of the decorated function count times. When we apply @repeat(count=2) to hello, it will execute the wrapper function twice, printing the message:

1
2
Hello, Bob!
Hello, Bob!

Wrapping Classes with Decorators

Decorators can also be used to wrap classes, enabling you to modify the behavior of methods or add new methods altogether.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def uppercase(cls):
    def wrapper(*args, **kwargs):
        instance = cls(*args, **kwargs)
        for attr, value in vars(instance).items():
            if callable(value):
                setattr(instance, attr, lambda *args: value(*args).upper())
        return instance
    return wrapper

@uppercase
class Greeter:
    def greet(self, name):
        return f"Hello, {name}!"

greeter = Greeter()
greeter.greet("Alice")

In this example, the uppercase decorator wraps the Greeter class. It creates an instance of the class, modifies its methods to return the uppercase result, and returns the modified instance. When we call greeter.greet("Alice"), it will output:

1
HELLO, ALICE!

Conclusion

Python decorators are a powerful feature that allows you to add custom functionality to functions or classes with ease. They enable you to separate cross-cutting concerns and make your code more modular and reusable. By understanding the concept and applying it effectively, you can enhance your Python code and create more expressive, flexible software.


➡️ ESP32 vs nRF52: A Comparison


⬅️ Getting Started with ESP32 Development


Go back to Posts.