In [148]:
class myDecorator:
    def __init__(self, f):
        print("Initializing myDecorator")
        f()
    def __call__(self):
        print("I am Inside __call__")

@myDecorator
def my_function():
    print("I am in my_function")

print("Decoration is finished...")
Initializing myDecorator
I am in my_function
Decoration is finished...
In [149]:
my_function()
I am Inside __call__

When a Function() is called after it has been decorated, we get completely different behavior; the my_decorator.call() method is called instead of the original code. That’s because the act of decoration replaces the original function object with the result of the decoration. classes we use as decorators must implement __call__.

Another Example

In [150]:
class hello:
    def __init__(self, f):
        self.f = f
    def __call__(self):
        print("Here i am: " + self.f.__name__ )
        self.f()
In [151]:
@hello
def func1():
    print("I am function 1")

@hello
def func2():
    print("I am Function 2")
In [152]:
func1()
func2()
Here i am: func1
I am function 1
Here i am: func2
I am Function 2

Decorator Without Argument : If we create a decorator without arguments, the function to be decorated is passed to the constructor, and the __call__() method is called whenever the decorated function is invoked. Any arguments for the decorated function are just passed to __call__()

In [153]:
class decorator_without_arguments:
    def __init__(self, f):
        self.f = f 
        print("Initializing Decorator for: " + self.f.__name__)
    def __call__(self, a, b):
        print("Inside Function: " + self.f.__name__)
        self.f(a, b)
In [154]:
@decorator_without_arguments
def func1(a, b):
    print("Func Args: {}, {}".format(a,b))

@decorator_without_arguments
def func2(a, b):
    print("Func Args: {}, {}".format(a,b))
Initializing Decorator for: func1
Initializing Decorator for: func2
In [155]:
func1(1,2)
func2(3,4)
Inside Function: func1
Func Args: 1, 2
Inside Function: func2
Func Args: 3, 4

Decorator With Argument: The decorator mechanism behaves quite differently when you pass arguments to the decorator.

Let’s modify the above example to see what happens when we add arguments to the decorator:

In [156]:
class decorator_with_arguments:
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2
    
    def __call__(self ,f):
        print("Inside __call__(): " + f.__name__)
        print("Decorator Args: {}, {}".format(self.arg1, self.arg2))
        def inner(a,b):
            f(a,b)
        return inner
In [157]:
@decorator_with_arguments("a", "b")
def func1(a, b):
    print("Args: {}, {}".format(a,b))

@decorator_with_arguments("c", "d")
def func2(a, b):
    print("Args: {}, {}".format(a,b))
Inside __call__(): func1
Decorator Args: a, b
Inside __call__(): func2
Decorator Args: c, d

Now the process of decoration calls the constructor and then immediately invokes __call__(), which can only take a single argument (the function object) and must return the decorated function.

In [158]:
func1(1,2)
func2(3,4)
Args: 1, 2
Args: 3, 4

Using Function as Decorator

In [159]:
def decoratorHello(func):
    print("Initializing decorator for: " + func.__name__)
    def inner(a,b):
        func(a,b)
    return inner
In [160]:
@decoratorHello
def func1(a,b):
    print("I am func1, Args: {}, {}".format(a,b))

@decoratorHello
def func2(a,b):
    print("I am func2, Args: {}, {}".format(a,b))
Initializing decorator for: func1
Initializing decorator for: func2
In [161]:
func1(1,2)
func2(3,4)
I am func1, Args: 1, 2
I am func2, Args: 3, 4

Decorator Function Without Argument : The above example is the one, Any arguments for the decorated function are just passed to inner()

Decorated Function With Argument:

In [162]:
def decoratorHello(arg1, arg2):
    def wrap(func):
        print("Initializing decorator for: " + func.__name__)
        print("Decorator Arg: {}, {}".format(arg1, arg2))
        def inner(a,b):
            func(a,b)
        return inner
    return wrap
In [163]:
@decoratorHello("a" ,"b")
def func1(a,b):
    print("Args: {}, {}".format(a,b))

@decoratorHello("c", "d")
def func2(a,b):
    print("Args: {}, {}".format(a,b))
Initializing decorator for: func1
Decorator Arg: a, b
Initializing decorator for: func2
Decorator Arg: c, d
In [165]:
func1(1,2)
func2(2,3)
Args: 1, 2
Args: 2, 3

The return value of the decorator function must be a function used to wrap the function to be decorated. That is, Python will take the returned function and call it at decoration time, passing the function to be decorated. That’s why we have three levels of functions; the inner one is the actual replacement function.

Because of closures, wrapped_f() has access to the decorator arguments arg1, arg2 and arg3, without having to explicitly store them as in the class version. However, this is a case where I find “explicit is better than implicit,” so even though the function version is more succinct I find the class version easier to understand and thus to modify and maintain.