Flevo CFD

Decorators in Python

Decorator is a function that returns another function. the goal of decorators is to add logic to existing functions. common use cases could be log the output, check the permissions before running the function and …

Take the following code as an example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def log(func):
    def wrapper(*args, **kwargs):
        print(f"executing {func} with arguments {args} {kwargs}")
        return func(*args, **kwargs)
    return wrapper


def test():
    print("hello")

test = log(test)

The log function is a decorator. it takes a function as an argument and returns another function. the returned function is the one that will be executed when we call test. the wrapper function is the one that will be executed when we call test. it logs the arguments and the function name before executing the function.

The sugar syntax is

1
2
3
@log
def test():
    print("hello")

What if we had multiple decorators?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
def decorator1(func):
    print("inside decorator1")
    def wrapper(*args, **kwargs):
        print(f"inside decorator1 wrappper")
        print(f"executing {func}")
        return func(*args, **kwargs)
    return wrapper


def decorator2(func):
    print("inside decorator2")
    def wrapper(*args, **kwargs):
        print(f"inside decorator2 wrappper")
        print(f"executing {func}")
        return func(*args, **kwargs)
    return wrapper


@decorator1
@decorator2
def test():
    print("hello")

What’s the output of the code? the above code is just definition of functions. it doesn’t run any function. with that being said, let’s uncompress the decorator syntax

1
2
3
4
def test():
    print("hello")

test = decorator1(decorator2(test))

So the output is

inside decorator2
inside decorator1

If we add the following line to the code

1
test()

The output would be

inside decorator2
inside decorator1
inside decorator1 wrappper
executing <function decorator2.<locals>.wrapper at 0x7f48ee8113f0>
inside decorator2 wrappper
executing <function test at 0x7f48ee811360>
hello

Decorators with arguments

A decorator without argument was like

1
test = decorator(test)

We want to add arguments to the decorator. how can we rewrite it?

1
test = decorator(*decorator_args, **decorator_kwargs)(test)

To use the sugar syntaxt

1
2
3
4
5
6
def decorator(*decorator_args, **decorator_kwargs)
    def inner(func):
        def wrapper(*args, **kwarg):
            return func(*args, **kwargs)
        return wrapper
    return inner

You can compare it with the decorator without argument

1
2
3
4
def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def decorator(*decorator_args, **decorator_kwargs):
    print(f"decorator arguments {decorator_args} {decorator_kwargs}")
    def inner(func):
        def wrapper(*args, **kwargs):
            print(f"inside wrappper")
            print(f"executing {func} with arguments {args} {kwargs}")
            return func(*args, **kwargs)
        return wrapper
    return inner


@decorator(1, 2)
def test(*args, **kwargs):
    """Test function"""
    print(f"function arguments {args} {kwargs}")
    print("hello")

test(3, 4)

Output

1
2
3
4
5
decorator arguments (1, 2) {}
inside wrappper
executing <function test at 0x7f1338b0d360> with arguments (3, 4) {}
function arguments (3, 4) {}
hello

The actual function which is test is replaced by wrapper which as a consequence replaces the name and docstring of it

print(test.__name__)
print(test.__doc__)

Output

wrapped
None

To preserve those attribute, we can use wraps decorator on the wrapped function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from functools import wraps


def decorator(*decorator_args, **decorator_kwargs):
    print(f"decorator arguments {decorator_args} {decorator_kwargs}")
    def inner(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"inside wrappper")
            print(f"executing {func} with arguments {args} {kwargs}")
            return func(*args, **kwargs)
        return wrapper
    return inner


@decorator(1, 2)
def test(*args, **kwargs):
    """Test function"""
    print(f"function arguments {args} {kwargs}")
    print("hello")

And the output of the preivous code would be

test
Test function

We saw some example of function decorators. what if we want to use them on a class or instance method? can you guess what we need to change?

The difference is that a method has by default one arguemnet which is the class instance or class itself. so basically it’s the same as function decorator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def method_decorator(func):
    print("inside decorator")
    def wrapper(self, *args, **kwargs):
        print(f"inside wrappper")
        print(f"executing {func} with arguments {args} {kwargs}")
        return func(self)
    return wrapper


class A:
    @method_decorator
    def test(self):
        print("hello")

A().test()

Output

inside decorator
inside wrappper
executing <function A.test at 0x7f8d1cb0d360> with arguments () {}
hello