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.
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:
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.
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:
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:
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:
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:
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.
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:
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.
from decorator import do_twice
@do_twice
def say_hello():
print("Hello There")
say_hello()
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:
from decorator import do_twice
@do_twice
def display(name):
print(f"Hello {name}")
display()
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:
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.
from decorator import do_twice
@do_twice
def display(name):
print(f"Hello {name}")
display("John")
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:
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:
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:
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:
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:
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:
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:
@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:
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:
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:
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:
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.
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:
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.