Skip to content

1.4 Decorators

Bora Canbula edited this page Dec 2, 2023 · 1 revision

Decorators take a function as argument and returns a function. They are used to extend the behavior of the wrapped function, without modifying it. So they are very useful for dealing with code legacy.

Decorators as a Design Pattern

You can define decorators in any programming language by using the following design pattern:

def my_decorator(fn):
    def _my_decorator():
        print("Before the function")
        fn()
        print("After the function")
    return _my_decorator

def my_decorated_function():
    print("Function")

# with this usage you should create a new object once by using decorator manually
my_decorated_function = my_decorator(my_decorated_function)

Note that we are using a single leading underscore to follow the "inner use" convention.

Decorators in Python

In Python you can relate your functions with decorators by using a line starting with @ before the function's signature.

def my_decorator(fn):
    def _my_decorator():
        print("Before the function")
        fn()
        print("After the function")
    return _my_decorator

@my_decorator
def my_decorated_function():
    print("Function")

Decorators with Arguments

Most of the time you should also pass the arguments to the wrapped functions. You can use *args and **kwargs to do this easily. You can also modify the input arguments or return values as you wish.

def decorator(func):
    def _decorator(*args, **kwargs):
        print("Before the function")
        print(args)
        print(kwargs)
        func(*args, **kwargs)
        print("After the function")
    return _decorator

@decorator
def decorated_func_w_args(x):
    print(f"x = {x}")

You may also want to use separate arguments for a decorator. You can create a decorator which can accept its own parameters by adding one more level as follows:

def decorator(arg1, arg2):
    def real_decorator(func):
        def _decorator(*args, **kwargs):
            print(f"The arguments are: {arg1}, {arg2}")
            print("Before the function")
            print(args)
            print(kwargs)
            func(*args, **kwargs)
            print("After the function")
        return _decorator
    return real_decorator

@decorator("Bora", "Canbula")
def my_function(s):
    print(s)

my_function("Parallel Programming")

If you need to use a more complex setup, you can also define your decorator as a class by using its __call__ method:

class DecoratorClass:
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2

    def __call__(self, func):
        def decorator(*args, **kwargs):
            print(f"The arguments are: {self.arg1}, {self.arg2}")
            print("Before the function")
            print(args)
            print(kwargs)
            func(*args, **kwargs)
            print("After the function")
        return decorator

@DecoratorClass("Bora", "Canbula")
def my_function(s):
    print(s)

my_function("Parallel Programming")

Decorator Chain

In Python, it is allowed to use more than one decorator, so you can create a decorator chain:

@decorator("Celal", "Bayar")
@DecoratorClass("Bora", "Canbula")
def my_function(s):
    print(s)

my_function("Parallel Programming")

This is the end of the section 0, which is pre-required knowledge for Parallel Programming course. You can test yourself by writing a decorator, which checks the type hints of arguments and raises TypeError if the values are not relevant with the types.