Wednesday, August 31, 2022

@decorators in Python

People have confusion about how to use python decorators in the proper way and how it works. The main reason for it is there are several ways to use decorators. In this article, we will discuss the basics of decoration and demonstrate all types of uses. 

First, we will learn a few terminologies.

First-class objects in a programming language are entities that behave just like normal objects. They can be referenced by variables, stored in data structures like list or dict, passed as arguments to another function, returned as a value from another function.
In mathematics and computer science, a higher-order function (HOF) is a function that does at least one of the following:
  • takes one or more functions as arguments (i.e. a procedural parameter, which is a parameter of a procedure that is itself a procedure),
  • returns a function as its result.
All other functions are first-order functions. 

Functions in Python are first class citizens. This means that they support operations such as being passed as an argument, returned from a function, modified, and assigned to a variable. This is a fundamental concept to understand before we delve into creating Python decorators. Let's checkout the code's from datacamp.com.

Assigning Functions to Variables
def plus_one(number):
    return number + 1

add_one = plus_one
add_one(5)
"""
output:

6
"""
Defining Functions Inside other Functions

def plus_one(number):
    def add_one(number):
        return number + 1


    result = add_one(number)
    return result
plus_one(4)
"""
output:

5
"""

Passing Functions as Arguments to other Functions


def plus_one(number):
    return number + 1

def function_call(function):
    number_to_add = 5
    return function(number_to_add)

function_call(plus_one)
"""
output:

6
"""
Functions Returning other Functions

def hello_function():
    def say_hi():
        return "Hi"
    return say_hi
hello = hello_function()
hello()
"""
output:

Hi
"""
Creating Decorators

def my_decorator(func):
    def wrapper():
        print("Before the func call.")
        func()
        print("After the func call.")

    return wrapper


def say_hello():
    print("Hello!")


say_hello = my_decorator(say_hello)
say_hello()
"""
output:

Before the func call.
Hello!
After the func call.
"""
Now same thing will be done by using @ symbol

def my_decorator(func):
    def wrapper():
        print("Before the func call.")
        func()
        print("After the func call.")

    return wrapper


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


say_hello()
"""
output:

Before the func call.
Hello!
After the func call.
"""
So, @my_decorator is just an easier way of saying say_whee = my_decorator(say_whee). It’s how you apply a decorator to a function.


Now I will demonstrate 7 types of decorator implementation examples. These are
  1. Class implementation of function decorator without argument
  2. Function implementation of function decorator without argument
  3. Class implementation of function decorator with argument
  4. Function implementation of function decorator with argument
  5. Class implementation of class decorator without argument
  6. Function implementation of class decorator without argument
  7. Decorator chaining 
All these example codes are available at github.com

Class implementation of function decorator without argument

class MyClassDecorator(object):

    def __init__(self, f):
        print('__init__() called | function:' + str(f.__name__))
        self.f = f

    def __call__(self, *args, **kwargs):
        print('__call__() called | function:' + str(self.f.__name__) + ' | args: ' + str(args) + ' | kwargs:' + str(
            kwargs))
        self.f(*args, **kwargs)


@MyClassDecorator
def display_function(a, b, c):
    print("display_function() called")


@MyClassDecorator
def display_function_no_call(a, b, c):
    print("display_function_no_call() called")


print("Decoration finished for display_function() and display_function_no_call()")

display_function(1, 2, 3)
print("display_function() executed")

"""
output:

__init__() called | function:display_function
__init__() called | function:display_function_no_call
Decoration finished for display_function() and display_function_no_call()
__call__() called | function:display_function | args: (1, 2, 3) | kwargs:{}
display_function() called
display_function() executed
"""

Function implementation of function decorator without argument

def my_decorator(f):
    print('my_decorator() called | function:' + str(f.__name__))

    def wrapped(*args, **kwargs):
        print('wrapped() called |  function:' + str(f.__name__) + ' | args:' + str(args) + '| kwargs:' + str(kwargs))
        f(*args)  # calling the original function

    return wrapped


@my_decorator
def display_function(a, b, c):
    print("display_function() called")


@my_decorator
def display_function_no_call(a, b, c):
    print("display_function_no_call() called")


print("Decoration finished for display_function() and display_function_no_call()")

display_function(1, 2, 3)
print("display_function() executed")

"""
output:

my_decorator() called | function:display_function
my_decorator() called | function:display_function_no_call
Decoration finished for display_function() and display_function_no_call()
wrapped() called |  function:display_function | args:(1, 2, 3)| kwargs:{}
display_function() called
display_function() executed
"""

Class implementation of function decorator with argument

class MyClassDecorator(object):

    def __init__(self, *deco_args, **deco_kwargs):
        print('__init__() called  | args: ' + str(deco_args) + ' | kwargs:' + str(deco_kwargs))

    def __call__(self, f):
        print('__call__() called | function:' + str(f.__name__))

        def wrapped(*args, **kwargs):
            print(
                'wrapped() called |  function:' + str(f.__name__) + ' | args:' + str(args) + '| kwargs:' + str(kwargs))
            return f(*args, **kwargs)

        return wrapped


@MyClassDecorator("arg1", "arg2")
def display_function(a, b, c):
    print("display_function() called")


@MyClassDecorator("no_call_arg1", "no_call_arg2")
def display_function_no_call(a, b, c):
    print("display_function_no_call() called")


print("Decoration finished for display_function() and display_function_no_call()")

display_function(1, 2, 3)
print("display_function() executed")

"""
output:

__init__() called  | args: ('arg1', 'arg2') | kwargs:{}
__call__() called | function:display_function
__init__() called  | args: ('no_call_arg1', 'no_call_arg2') | kwargs:{}
__call__() called | function:display_function_no_call
Decoration finished for display_function() and display_function_no_call()
wrapped() called |  function:display_function | args:(1, 2, 3)| kwargs:{}
display_function() called
display_function() executed
"""
Function implementation of function decorator with argument

def my_decorator(*deco_args, **deco_kwargs):
    print('my_decorator() called | args: ' + str(deco_args) + ' | kwargs:' + str(deco_kwargs))

    def inner(f):
        print('inner(f) called  |  function:' + str(f.__name__) + ' || ( Have access to  deco_args: ' + str(
            deco_args) + ' | deco_kwargs:' + str(deco_kwargs) + ')')

        def wrapped(*args, **kwargs):
            print(
                'wrapped() called |  function:' + str(f.__name__) + ' | args:' + str(args) + '| kwargs:' + str(
                    kwargs) + ' || ( Have access to  deco_args: ' + str(
                    deco_args) + ' | deco_kwargs:' + str(deco_kwargs) + ')')
            return f(*args, **kwargs)  # calling the original function and return

        return wrapped

    return inner


@my_decorator("arg1", "arg2")
def display_function(a, b, c):
    print("display_function() called")


@my_decorator("no_call_arg1", "no_call_arg2")
def display_function_no_call(a, b, c):
    print("display_function_no_call() called")


print("Decoration finished for display_function() and display_function_no_call()")

display_function(1, 2, 3)
print("display_function() executed")

"""
output:

my_decorator() called | args: ('arg1', 'arg2') | kwargs:{}
inner(f) called  |  function:display_function || ( Have access to  deco_args: ('arg1', 'arg2') | deco_kwargs:{})
my_decorator() called | args: ('no_call_arg1', 'no_call_arg2') | kwargs:{}
inner(f) called  |  function:display_function_no_call || ( Have access to  deco_args: ('no_call_arg1', 'no_call_arg2') | deco_kwargs:{})
Decoration finished for display_function() and display_function_no_call()
wrapped() called |  function:display_function | args:(1, 2, 3)| kwargs:{} || ( Have access to  deco_args: ('arg1', 'arg2') | deco_kwargs:{})
display_function() called
display_function() executed
"""
Class implementation of class decorator without argument

class MyClassDecorator:
    # accept the class as argument
    def __init__(self, _class):
        print('__init__() called | class:' + str(_class.__name__))
        self._class = _class

    # accept the class's __init__ method arguments
    def __call__(self, name):
        print('__call__() called | class:' + str(self._class.__name__) + ' | arg:' + str(name))

        # define a new display method
        def new_display(self):
            print('new_display() called')
            print('Name: ', self.name)
            print('PIN: 1234')

        # replace display with new_display
        self._class.display = new_display

        # return the instance of the class
        obj = self._class(name)
        print('returning modified class object')
        return obj


@MyClassDecorator
class Employee:
    def __init__(self, name):
        print('original __init__() called' + ' | arg:' + str(name))
        self.name = name

    def display(self):
        print('original display() called')
        print('Name: ', self.name)


print("Decoration finished for Employee Class")
obj = Employee('Towhidul Haque Roni')
print("Employee obj created")
obj.display()
print("display() executed")

"""
output:

__init__() called | class:Employee
Decoration finished for Employee Class
__call__() called | class:Employee | arg:Towhidul Haque Roni
original __init__() called | arg:Towhidul Haque Roni
returning modified class object
Employee obj created
new_display() called
Name:  Towhidul Haque Roni
PIN: 1234
display() executed
"""

Function implementation of class decorator without argument

def my_decorator(_class):
    print('my_decorator() called for the class: ' + str(_class.__name__))

    # define a new display method
    def new_display(self):
        print('new_display() called')
        print('Name: ', self.name)
        print('PIN: 1234')

    # replace the display with new_display
    # (if the display method did not exist in the class,
    # the new_display would have been added to the class as the display method)
    _class.display = new_display

    # return the modified employee
    print('returning modified class (not object)')
    return _class


@my_decorator
class Employee:
    def __init__(self, name):
        print('original __init__() called' + ' | arg:' + str(name))
        self.name = name

    def display(self):
        print('original display() called')
        print('Name:', self.name)


print("Decoration finished for Employee Class")
obj = Employee('Towhidul Haque Roni')
print("Employee obj created")
obj.display()
print("display() executed")

"""
output:

my_decorator() called for the class: Employee
returning modified class (not object)
Decoration finished for Employee Class
original __init__() called | arg:Towhidul Haque Roni
Employee obj created
new_display() called
Name:  Towhidul Haque Roni
PIN: 1234
display() executed
"""
Decorator chaining 

def register(*decorators):
    """
    This decorator is for chaining multiple decorators.
    :param decorators:args(the decorators as arguments)
    :return: callable object
    """

    def register_wrapper(func):
        for deco in decorators[::-1]:
            func = deco(func)
        func._decorators = decorators
        return func

    return register_wrapper


def deco1(f):
    def wrapper(*args, **kwds):
        print('-' * 100)
        fn = f(*args, **kwds)
        print('-' * 100)
        return fn

    return wrapper


def deco2(f):
    def wrapper(*args, **kwds):
        print('*' * 100)
        fn = f(*args, **kwds)
        print('*' * 100)
        return fn

    return wrapper


def deco3(f):
    def wrapper(*args, **kwds):
        print('#' * 100)
        fn = f(*args, **kwds)
        print('#' * 100)
        return fn

    return wrapper


class Foo(object):
    @deco1
    @deco2
    @deco3
    def bar(self):
        print('I am bar')


class AnotherFoo(object):
    @register(deco1, deco2, deco3)
    def bar(self):
        print('I am bar')


foo = Foo()
foo.bar()
print('\n\n~~~~ Alternate Way to Annotate ~~~~\n\n')
another_foo = AnotherFoo()
another_foo.bar()
print(another_foo.bar._decorators)


"""
output:

----------------------------------------------------------------------------------------------------
****************************************************************************************************
####################################################################################################
I am bar
####################################################################################################
****************************************************************************************************
----------------------------------------------------------------------------------------------------


~~~~ Alternate Way to Annotate ~~~~


----------------------------------------------------------------------------------------------------
****************************************************************************************************
####################################################################################################
I am bar
####################################################################################################
****************************************************************************************************
----------------------------------------------------------------------------------------------------
(<function deco1 at 0x7f50c7e6c940>, <function deco2 at 0x7f50c7e6c9d0>, <function deco3 at 0x7f50c7e6ca60>)

"""
You can comment any suggestions on it. If you enjoyed the article and want updates about my new article, please follow me.


@decorators in Python

People have confusion about how to use python decorators in the proper way and how it works. The main reason for it is there are several way...