Python Decorators Tutorial with Examples

Decorators represent one of the most beneficial and potent features available in Python. They serve the purpose of altering the behavior of a function. By utilizing decorators, you gain the ability to encapsulate a different function, thereby enhancing the functionality of the original function without making any permanent changes to it.

This concept is often referred to as meta programming, wherein a segment of the program strives to modify a different segment of the program during the compilation process.

Prior to delving into the Decorator concept, it is essential to familiarize ourselves with several key principles of Python.

What are the functions in Python?

Python possesses a particularly fascinating characteristic where everything is considered an object. This includes not only the classes but also any variables we create within Python, which are likewise regarded as objects. In Python, functions are classified as first-class objects since they can be assigned to variables, passed around as arguments, and returned from other functions. The following example illustrates this concept:

Python Function Example

Let’s examine an example to illustrate the functionality in Python.

Example

def func1(msg):    # here, we are creating a function and passing the parameter

    print(msg)

func1("Hii, welcome to function ")   # Here, we are printing the data of function 1

func2 = func1      # Here, we are copying the function 1 data to function 2

func2("Hii, welcome to function ")   # Here, we are printing the data of function 2

Output:

Output

Hii, welcome to function

Hii, welcome to function

Clarification: In the program presented above, executing the code yields identical results for both functions. The func2 function acts as an alias for func1, effectively behaving as a function itself. It is essential to grasp the following concepts regarding functions:

  • Functions can be assigned to variables, referenced, and returned from other functions.
  • Functions can also be defined within the scope of another function and can be passed as parameters to yet another function.
  • Inner Function

Python allows for the creation of a function within the scope of another function. Such functions are referred to as inner functions. Take a look at the example below:

Python Inner Function Example

Let’s examine an example to illustrate the concept of inner functions in Python.

Example

def func():    # here, we are creating a function and passing the parameter

     print("We are in first function")      # Here, we are printing the data of function

     def func1():      # here, we are creating a function and passing the parameter

           print("This is first child function")  # Here, we are printing the data of function 1

     def func2():      # here, we are creating a function and passing the parameter

           print("This is second child function")      # Here, we are printing the data of         # function 2

     func1()

     func2()

func()

Output:

Output

We are in first function

This is first child function

This is second child function

Clarification: In the program provided above, the manner in which the child functions are defined is irrelevant. What truly influences the output is the execution of these child functions. These child functions are scoped locally within func, which means they cannot be invoked independently.

Higher Order Function

A function that takes another function as an argument is referred to as a higher-order function. Take a look at the example below:

Example:

Example

def add(x):          # here, we are creating a function add and passing the parameter

    return x+1       # here, we are returning the passed value by adding 1

def sub(x):          # here, we are creating a function sub and passing the parameter

    return x-1        # here, we are returning the passed value by subtracting 1

def operator(func, x):    # here, we are creating a function and passing the parameter

    temp = func(x)

    return temp

print(operator(sub,10))  # here, we are printing the operation subtraction with 10

print(operator(add,20))   # here, we are printing the operation addition with 20

Output:

Clarification: In the program presented above, the sub function and the add function have been supplied as parameters to the operator function.

A function has the capability to yield another function as its output. Examine the following illustration:

Example:

Example

def hello():         # here, we are creating a function named hello

    def hi():         # here, we are creating a function named hi

        print("Hello")             # here, we are printing the output of the function

    return hi         # here, we are returning the output of the function

new = hello()

new()

Output:

Clarification: In the program provided, the hi function is encapsulated within the hello function. It will yield a return value each time hi is invoked.

Decorating functions with parameters

To illustrate the concept of a parameterized decorator function, let’s consider an example:

Example:

Example

def divide(x,y):       # here, we are creating a function and passing the parameter

    print(x/y)         # Here, we are printing the result of the expression

def outer_div(func):      # here, we are creating a function and passing the parameter

    def inner(x,y):      # here, we are creating a function and passing the parameter

        if(x<y):

            x,y = y,x

           return func(x,y)

# here, we are returning a function with some passed parameters

     return inner

divide1 = outer_div(divide)

divide1(2,4)

Output:

Syntactic Decorator

In the preceding program, the function out_div has been adorned with a decoration that is somewhat extensive. Rather than employing the previously mentioned approach, Python provides a more straightforward way to utilize decorators by using the @ symbol. This method is occasionally referred to as "pie" syntax.

Syntactoc Decorator Example:

Let’s examine a case that illustrates the use of syntactic decorators in Python.

Example

def outer_div(func):     # here, we are creating a function and passing the parameter

    def inner(x,y):        # here, we are creating a function and passing the parameter

        if(x<y):

           x,y = y,x

          return func(x,y)       # here, we are returning the function with the parameters

     return inner

# Here, the below is the syntax of generator

@outer_div

def divide(x,y):      # here, we are creating a function and passing the parameter

     print(x/y)

Output:

Reusing Decorator

We can also reuse the decorator by referencing the decorator function again. To facilitate this, let's separate the decorator into its own module, allowing for its utilization across various functions. We will create a file named mod_decorator.py containing the following code:

Example

def do_twice(func):      # here, we are creating a function and passing the parameter

    def wrapper_do_twice():

     # here, we are creating a function and passing the parameter

        func()

        func()

    return wrapper_do_twice

We can import mod_decorator.py in another file.

from decorator import do_twice

@do_twice

def say_hello():

    print("Hello There")

say_hello()

We can import mod_decorator.py in other file.

Example

from decorator import do_twice

@do_twice

def say_hello():

    print("Hello There")

say_hello()

Output:

Output

Hello There

Hello There

Python Decorator with Argument

We aim to transmit several arguments to a function. Let’s accomplish this with the following code:

Example

from decorator import do_twice

@do_twice

def display(name):

     print(f"Hello {name}")

display()

Output:

Output

TypeError: display() missing 1 required positional argument: 'name'

It is evident that the function did not take the argument. Executing this code results in an error. We can resolve this issue by incorporating args and *kwargs into the inner wrapper function. We need to update decorator.py in the following manner:

Example

def do_twice(func):

    def wrapper_function(*args,**kwargs):

        func(*args,**kwargs)

        func(*args,**kwargs)

   return wrapper_function

The function wrapper_function is now capable of accepting an arbitrary number of arguments and forwarding them to the designated function.

Example

from decorator import do_twice

@do_twice

def display(name):

      print(f"Hello {name}")

display("John")

Output:

Output

Hello John

Hello John

Returning Values from Decorated Functions

We have the ability to manipulate the return type of the function that is being decorated. An illustration of this is provided below:

Example

from decorator import do_twice

@do_twice

def return_greeting(name):

     print("We are created greeting")

     return f"Hi {name}"

hi_adam = return_greeting("Adam")

Output:

Output

We are created greeting

We are created greeting

Fancy Decorators

Let’s delve into the intriguing world of decorators by examining the following subject:

Class Decorators

Python offers two approaches for decorating a class. To begin with, we can apply decorators to methods within a class; Python includes several built-in decorators such as @classmethod, @staticmethod, and @property. The @classmethod and @staticmethod decorators create methods within a class that do not have a direct association with any specific instance of that class. Meanwhile, @property is typically utilized to alter the getters and setters for the attributes of a class. To illustrate this concept, let us consider the following example:

Example: 1-

The @property decorator allows us to utilize a class method as if it were an attribute. Take a look at the code example below:

Example

class Student:     # here, we are creating a class with the name Student

    def __init__(self,name,grade):

         self.name = name

         self.grade = grade

    @property

    def display(self):

         return self.name + " got grade " + self.grade

stu = Student("John","B")

print("Name of the student: ", stu.name)

print("Grade of the student: ", stu.grade)

print(stu.display)

Output:

Output

Name of the student: John

Grade of the student: B

John got grade B

Example: 2-

The @staticmethod decorator is employed to establish a static method within a class. This method can be invoked using both the class name and an instance of the class. Examine the code below:

Example

class Person:       # here, we are creating a class with the name Student

     @staticmethod

     def hello():         # here, we are defining a function hello

          print("Hello Peter")

per = Person()

per.hello()

Person.hello()

Output:

Output

Hello Peter

Hello Peter

Singleton Class

A singleton class is characterized by having a single instance throughout the lifetime of an application. In Python, there are numerous examples of singletons, such as True, None, and others.

Nesting Decorators

It is possible to apply several decorators simultaneously by stacking them. Let’s examine the following illustration:

Example

@function1

@function2

def function(name):

      print(f "{name}")

In the code presented above, we have implemented the concept of nested decorators by layering them on top of each other.

Decorator with Arguments

Utilizing arguments within a decorator is often advantageous. This allows the decorator to be invoked multiple times based on the specified argument value. We can illustrate this with the following example:

Example:

Example

Import functools      # here, we are importing the functools into our program

def repeat(num):     # here, we are defining a function repeat and passing parameter

# Here, we are creating and returning a wrapper function

    def decorator_repeat(func):

        @functools.wraps(func)

        def wrapper(*args,**kwargs):

            for _ in range(num):  # here, we are initializing a for loop and iterating till num

                value = func(*args,**kwargs)

             return value      # here, we are returning the value

          return wrapper    # here, we are returning the wrapper class

    return decorator_repeat

#Here we are passing num as an argument which repeats the print function

@repeat(num=5)

def function1(name):

     print(f"{name}")

Output:

Output

logicpractice

logicpractice

logicpractice

logicpractice

logicpractice

In the preceding example, @repeat denotes a function object that may be utilized within another function. The invocation of @repeat(num = 5) yields a function that operates as a decorator.

The code presented above might appear intricate; however, it exemplifies the widely utilized decorator pattern. In this instance, we have implemented an additional function that manages the arguments for the decorator.

Note: Decorator with argument is not frequently used in programming, but it provides flexibility. We can use it with or without argument.

Stateful Decorators

Stateful decorators serve the purpose of maintaining the state of the decorator. For illustration, let us examine a scenario where we develop a decorator that monitors the number of times a function has been invoked.

Example:

Example

Import functools          # here, we are importing the functools into our program

def count_function(func):

# here, we are defining a function and passing the parameter func

@functools.wraps(func)

def wrapper_count_calls(*args, **kwargs):

wrapper_count_calls.num_calls += 1

print(f"Call{wrapper_count_calls.num_calls} of {func.__name__!r}")

return func(*args, **kwargs)

wrapper_count_calls.num_calls = 0

return wrapper_count_calls      # here, we are returning the wrapper call counts

@count_function

def say_hello():  # here, we are defining a function and passing the parameter

print("Say Hello")

say_hello()

say_hello()

Output:

Output

Call 1 of 'say_hello'

Say Hello

Call 2 of 'say_hello'

Say Hello

In the program outlined above, the state reflects the quantity of times the function has been invoked, which is maintained in .numcalls within the wrapper function. When we execute sayhello, it will present the count of the function's invocation.

Classes as Decorators

Utilizing classes is an optimal method for managing state. In this segment, we will explore the implementation of a class as a decorator. We will develop a class that includes an init method, accepting func as a parameter. Furthermore, the class must be designed to be callable, allowing it to represent the function being decorated.

To enable a class to be invoked as a callable, we define the special method call.

Example

import functools         # here, we are importing the functools into our program

class Count_Calls:       # here, we are creating a class for getting the call count

def __init__(self, func):

functools.update_wrapper(self, func)

self.func = func

self.num_calls = 0

def __call__(self, *args, **kwargs):

self.num_calls += 1

print(f"Call{self.num_calls} of {self.func.__name__!r}")

return self.func(*args, **kwargs)

@Count_Calls

def say_hello():  # here, we are defining a function and passing the parameter

print("Say Hello")

say_hello()

say_hello()

say_hello()

Output:

Output

Call 1 of 'say_hello'

Say Hello

Call 2 of 'say_hello'

Say Hello

Call 3 of 'say_hello'

Say Hello

The init function retains a reference to the method and can also perform any necessary setup tasks.

Input Required

This code uses input(). Please provide values below: