Validation in Python using Metaprogramming and Decorator- Advanced Python

Validation in Python using Metaprogramming and Decorator- Advanced Python

A Decorator is a special kind of declaration that can apply to a function to enhance its functionality. This is also called known as metaprogramming.

A Decorator is a special kind of declaration that can apply to a function to enhance its functionality. This is also called known as metaprogramming. Here we are trying to modify the functionality of a function on compile time. Metaprogramming is a cool concept in ant programing language. It can be used to apply certain checks before executing the actual function.
Let’s try to understand the use-case with an example.

Suppose we have a function divide), which accepts numerators and denominators. Here numerators and denominators should be real numbers. and denominators can’t be zero.Let’s implement the function

def is_real_number(num):
    return type(num) == int or type(num) == float


def divide(num, den):
    if not(is_real_number(num) and is_real_number(den)):
        raise ValueError("Invalid argument")
    if den == 0:
        raise ValueError("Number is not divisible by zero")
    return num/den


def add(*arg):
    if any([not(is_real_number(i)) for i in arg]):
        raise ValueError("Invalid argument")
    return sum(arg)


print(divide(10, 2))  # 5.0
print(add(1, 2, 3))  # 6
print(divide("10", 2))  # ValueError: Invalid argument
print(divide(10, 0))  # ValueError: Number is not divisible by zero

Explanation:
If you look at the above example functions, We have added a few extra checks to verify passed arguments. These extra checks can be applied using metaprogramming without modifying the actual functions.

You should know:

Before proceeding further, There are a few concepts that you should know. These concepts will help to understand decorators.

  1. Everything is an object
  2. Class is callable

Everything is an object: In python, everything is an object. Yes, class is also an object. A name can be bound to multiple variables including function. Check out the below example.

def greet(name):
    print(f"Welcome Mr/Ms. {name}")


gr_8 = greet
greet("Deepak")  # Welcome Mr/Ms. Deepak
gr_8("Deepak")  # Welcome Mr/Ms. Deepak

We can also pass functions as arguments and use that functions.

def inc(x):
    return x + 1


def dec(x):
    return x - 1


def operate(func, x):
    result = func(x)
    return result


operate(inc, 5)  # 6
operate(dec, 5)  # 4

We can also return a function from a function.

def decorator():
    def inner():
        print("Inner function")
    return inner


outer = decorator()
outer()
# Inner function

Class is callable: You can make a class callable object by adding **def** **__call__**(self, *args, **kwargs) function. A callable object is an object but can be called a function. When we execute or call the callable object, it will internally call the method **__call__**

class Callable:
    def __init__(self):
        print("An instance of Callable was initialized")

    def __call__(self, *args, **kwargs):
        print("Arguments are:", args, kwargs)


x = Callable()
print("now calling the instance:")
x(3, 4, x=11, y=10)
"""
An instance of Callable was initialized
now calling the instance:
Arguments are: (3, 4) {'x': 11, 'y': 10}
"""

Now since we know the basic concepts, Let’s try to understand decorators.

Decorators using function

A Decorator function is a single argument function. It takes a function as an argument and returns a helper function.

def decorator(func):
    def inner():
        print("run something before actual function")
        func()
        print("run something after actual function")
    return inner


def func():
    print("something...")


decorated_func = decorator(func)
decorated_func()
"""
run something before actual function
something...
run something after actual function
"""

As you can see in the above example, the decorator function takes func as function and add before/after statements. A decorator functionality can be implemented using @decoratorexpression.

def decorator(func):
    def inner():
        print("run something before actual function")
        func()
        print("run something after actual function")
    return inner


@decorator
def func():
    print("something...")


func()
"""
run something before actual function
something...
run something after actual function
"""

You can also return a function that accepts varargs as an argument. This is very useful to override the function behaviour still gives full flexibility.

def prettify(func):
    def inner(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)
    return inner


@prettify
def pretty_print(*msg, **kwargs):
    print(*msg, **kwargs)


pretty_print("Hello World")
pretty_print(1, 2, 3)
"""
******************************
Hello World
******************************
******************************
1 2 3
******************************
"""

Decorators using Class

Just like function decorators, You can make callable object decorators. The biggest advantage of creating a class-based decorator is that it looks more declarative and clean compare to functional decorators.

To understand the concept, Let’s take our previous example of add and divide function. If you have noticed that both function as common validation of real numbers. We can create a common validator decorator class.

class RealNumValidator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwds):
        if any([not(is_real_number(i)) for i in args]):
            raise ValueError("Invalid argument")
        return self.func(*args)


class ZeroValidator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwds):
        if len(args) < 2 or args[1] == 0:
            raise ValueError("Number is not divisible by zero")
        return self.func(*args)

Now we have two argument validators. Let’s use this on real function.

class RealNumValidator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwds):
        if any([not(is_real_number(i)) for i in args]):
            raise ValueError("Invalid argument")
        return self.func(*args)


class ZeroValidator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwds):
        if len(args) < 2 or args[1] == 0:
            raise ValueError("Number is not divisible by zero")
        return self.func(*args)


def is_real_number(num):
    return type(num) == int or type(num) == float


@RealNumValidator
@ZeroValidator
def divide(num, den):
    return num/den


@RealNumValidator
def add(*arg):
    return sum(arg)


print(divide(10, 2))  # 5.0
print(add(1, 2, 3))  # 6
print(divide("10", 2))  # ValueError: Invalid argument
print(divide(10, 0))  # ValueError: Number is not divisible by zero

Now if you look at the functions, It looks much cleaner compare to its previous version.

Conclusion

There are a lot of things to be covered in metaprogramming or decorators. I have just touched the surface of the water. Like I have given an example of chaining of decorators. This can be very useful to share reusable validations and apply them. Tracing of function calls, spying of function, Writing custom parsers etc. are very common use cases of metaprogramming. You can explore and let me know in the comment.

References:

https://python-course.eu/advanced-python/decorators-decoration.php

Originally Posted: Validation in Python using Metaprogramming and Decorator- Advanced Python

Did you find this article valuable?

Support Deepak Vishwakarma by becoming a sponsor. Any amount is appreciated!