Editor's Image
Python, with its clean and readable syntax, is a widely used high-level programming language. Python is designed to be easy to use and emphasizes simplicity and low maintenance cost of the program. It comes with an extensive library that reduces the need for developers to write code from scratch and increases developer productivity. A powerful feature of Python that contributes to code elegance is decorators.
In Python, a decorator is a function that allows you to modify the behavior of another function without changing its core logic. Takes another function as an argument and returns the function with extended functionality. This way, you can use decorators to add some extra logic to existing functions to increase reusability with just a few lines of code. In this article, we'll explore eight built-in Python decorators that can help you write more elegant and maintainable code.
Editor's Image
He @atexit.register
The decorator is used to register a function to be executed upon program completion. This function can be used to perform any task when the program is about to exit, whether due to normal execution or an unexpected error.
Example:
import atexit
# Register the exit_handler function
@atexit.register
def exit_handler():
print("Exiting the program. Cleanup tasks can be performed here.")
# Rest of the program
def main():
print("Inside the main function.")
# Your program logic goes here.
if __name__ == "__main__":
main()
Production:
Inside the main function.
Exiting the program. Cleanup tasks can be performed here.
In the previous implementation, @atexit.register
It is mentioned above the function definition. Defines the exit_handler()
function as an output function. Basically, it means that every time the program reaches its termination point, whether through normal execution or due to an unexpected error that causes premature exit, the exit_handler()
The function will be invoked.
He @dataclasses.dataclass
is a powerful decorator used to automatically generate common special methods for classes like “__init__”, “__repr__” and others. It helps you write cleaner, more concise code by eliminating the need to write repetitive methods to initialize and compare instances of your class. It can also help prevent errors by ensuring that common special methods are implemented consistently throughout your code base.
Example:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
point = Point(x=3, y=2)
# Printing object
print(point)
# Checking for the equality of two objects
point1 = Point(x=1, y=2)
point2 = Point(x=1, y=2)
print(point1 == point2)
Production:
He @dataclass
The decorator, applied on top of the Point class definition, tells Python to use the default behavior to generate special methods. This automatically creates the __init__
method, which initializes class attributes, such as x and y, when instantiating the object. As a result, instances like point can be constructed without the need for explicit coding. Furthermore, the __repr__
The method, responsible for providing a string representation of objects, is also automatically wrapped. This ensures that when you print an object, such as a point, you get a clear and orderly representation, as seen in the result: Point(x=3, y=2). Additionally, the equality comparison (==) between two instances, point1 and point2, produces True. This is noteworthy because, by default, Python checks for equality based on memory location. However, in the context of data class objects, equality is determined by the data contained in them. This is because the @dataclass decorator generates a __eq__
Method that checks the equality of data present in objects, instead of checking the same memory location.
He @enum.unique
The decorator, found in the enum module, is used to ensure that the values of all members of an enum are unique. This helps prevent accidentally creating multiple enum members with the same value, which can lead to confusion and errors. If duplicate values are found, Value error is high.
Example:
from enum import Enum, unique
@unique
class VehicleType(Enum):
CAR = 1
TRUCK = 2
MOTORCYCLE = 3
BUS = 4
# Attempting to create an enumeration with a duplicate value will raise a ValueError
try:
@unique
class DuplicateVehicleType(Enum):
CAR = 1
TRUCK = 2
MOTORCYCLE = 3
# BUS and MOTORCYCLE have duplicate values
BUS = 3
except ValueError as e:
print(f"Error: {e}")
Production:
Error: duplicate values found in : BUS -> MOTORCYCLE
In the above implementation, “BUS” and “MOTORCYCLE” have the same value “3”. As a result, the @unique
The decorator raises a ValueError with a message indicating that duplicate values were found. You also cannot use the same key more than once or assign the same value to different members. This way it helps to avoid duplicate values for multiple members of the enum.
He partial
Decorator is a powerful tool used to create partial functions. Partial functions allow you to preset some of the arguments of the original function and generate a new function with those arguments already populated.
Example:
from functools import partial
# Original function
def power(base, exponent):
return base ** exponent
# Creating a partial function with the exponent fixed to 2
square = partial(power, exponent=2)
# Using the partial function
result = square(3)
print("Output:",result)
Production:
In the above implementation, we have a function “power” that accepts two arguments “base” and “exponent” and returns the result of the base raised to the power of the exponent. We have created a partial function called “square” using the original function in which the exponent is preset to 2. This way, we can extend the functionality of the original functions using a partial
decorator.
He @singledisptach
The decorator is used to create generic functions. Allows you to define different implementations of functions with the same name but different types of arguments. It's particularly useful when you want your code to behave differently for different types of data.
Example:
from functools import singledispatch
# Decorator
@singledispatch
def display_info(arg):
print(f"Generic: {arg}")
# Registering specialized implementations for different types
@display_info.register(int)
def display_int(arg):
print(f"Received an integer: {arg}")
@display_info.register(float)
def display_float(arg):
print(f"Received a float: {arg}")
@display_info.register(str)
def display_str(arg):
print(f"Received a string: {arg}")
@display_info.register(list)
def display_sequence(arg):
print(f"Received a sequence: {arg}")
# Using the generic function with different types
display_info(39)
display_info(3.19)
display_info("Hello World!")
display_info((2, 4, 6))
Production:
Received an integer: 39
Received a float: 3.19
Received a string: Hello World!
Received a sequence: (2, 4, 6)
In the above implementation, we first develop the generic function display_info()
using the @singledisptach
decorator and then registered its implementation for int, float, string and list separately. The result shows the operation of display_info()
for separate data types.
He @classmethod
is a decorator used to define class methods within the class. Class methods are bound to the class and not to the class object. The main distinction between static methods and class methods lies in their interaction with the state of the class. Class methods have access to and can modify the state of the class, while static methods cannot access the state of the class and operate independently.
Example:
class Student:
total_students = 0
def __init__(self, name, age):
self.name = name
self.age = age
Student.total_students += 1
@classmethod
def increment_total_students(cls):
cls.total_students += 1
print(f"Class method called. Total students now: {cls.total_students}")
# Creating instances of the class
student1 = Student(name="Tom", age=20)
student2 = Student(name="Cruise", age=22)
# Calling the class method
Student.increment_total_students() #Total students now: 3
# Accessing the class variable
print(f"Total students from student 1: {student1.total_students}")
print(f"Total students from student 2: {student2.total_students}")
Production:
Class method called. Total students now: 3
Total students from student 1: 3
Total students from student 2: 3
In the previous implementation, the Student the class has total_students as a class variable. He @classmethod
The decorator is used to define the increment_total_students()
class method to increase the total_students variable. Whenever we create an instance of the Student class, the total number of students is incremented by one. We create two instances of the class and then use the class method to modify the total_students variable a 3which is also reflected in the class instances.
He @staticmethod
The decorator is used to define static methods within a class. Static methods are methods that can be called without creating an instance of the class. Static methods are often used when they do not have to access object-related parameters and are more related to the class as a whole.
Example:
class MathOperations:
@staticmethod
def add(x, y):
return x + y
@staticmethod
def subtract(x, y):
return x - y
# Using the static methods without creating an instance of the class
sum_result = MathOperations.add(5, 4)
difference_result = MathOperations.subtract(8, 3)
print("Sum:", sum_result)
print("Difference:", difference_result)
Production:
In the above implementation, we have used @staticmethod
to define a static add() method for the “MathOperations” class. We have added the two numbers “4” and “5” which results in “9” without creating any instance of the class. Similarly, subtract the two numbers “8” and “3” to get “5.” In this way static methods can be generated to perform utility functions that do not require the state of an instance.
He @property
The decorator is used to define the getter methods for the class attribute. Getter methods are methods that return the value of an attribute. These methods are used for data encapsulation that specifies who can access the details of the class or instance.
Example:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
# Getter method for the radius.
return self._radius
@property
def area(self):
# Getter method for the area.
return 3.14 * self._radius**2
# Creating an instance of the Circle class
my_circle = Circle(radius=5)
# Accessing properties using the @property decorator
print("Radius:", my_circle.radius)
print("Area:", my_circle.area)
Production:
In the above implementation, the “Circle” class has a “radius” attribute. We have used @property
to set the getter methods for the radius and area. Provides a clean and consistent interface for users of the class to access these attributes.
This article highlights some of the most versatile and functional decorators you can use to make your code more flexible and readable. These decorators allow you to extend the functionality of the original function to make it more organized and less prone to errors. They are like magic touches that make your Python programs look clean and run smoothly.
Kanwal Mehreen is an aspiring software developer with a strong interest in data science and ai applications in medicine. Kanwal was selected as a Google Generation Scholar 2022 for the APAC region. Kanwal loves sharing technical knowledge by writing articles on trending topics and is passionate about improving the representation of women in the tech industry.