8 Built-in Python Decorators to Write Elegant Code
Image by Editor

 

Python, with its clean and readable syntax, is a widely used high-level programming language. Python is designed for ease of use that emphasizes simplicity and reduced cost of program maintenance. It comes with an extensive library that reduces the need for developers to write code from scratch and increases developers’ productivity. One 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. It 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 will explore eight built-in Python decorators that can help you write more elegant and maintainable code.

 

8 Built-in Python Decorators to Write Elegant Code
Image by Editor

 

 

The @atexit.register decorator is used to register a function to be executed at program termination. This function can be used to perform any task when the program is about to exit, whether it’s 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()

Output:

Inside the main function.
Exiting the program. Cleanup tasks can be performed here.

 

In the above implementation, @atexit.register is mentioned above the function definition. It defines the exit_handler() function as an exit function. Essentially, it means that whenever the program reaches its termination point, either through normal execution or due to an unexpected error causing a premature exit, the exit_handler() function will be invoked.

 

 

The @dataclasses.dataclass is a powerful decorator that is used to automatically generate common special methods for classes such as “__init__”, “__repr__” and others. It helps you write cleaner, more concise code by eliminating the need to write boilerplate methods for initializing and comparing instances of your class. It can also help prevent errors by ensuring that common special methods are implemented consistently across your codebase.

 

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)

Output:

 

The @dataclass decorator, applied above the Point class definition, signals Python to utilize default behavior for generating special methods. This automatically creates the __init__ method, which initializes class attributes, such as x and y, upon object instantiation. As a result, instances like point can be constructed without the need for explicit coding. Moreover, the __repr__ method, responsible for providing a string representation of objects, is also automatically adjusted. This ensures that when an object, like a point, is printed, it yields a clear and ordered representation, as seen in the output: 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 dataclass objects, equality is determined by the data contained within them. This is because the @dataclass decorator generates an __eq__ method that checks for the equality of the data present in the objects, rather than checking for the same memory location.

 

 

The @enum.unique decorator, found in the enum module, is used to ensure that the values of all the members of an enumeration are unique. This helps prevent the accidental creation of multiple enumeration members with the same value, which can lead to confusion and errors. If duplicate values are found, a ValueError is raised.

 

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}")

 

Output:

Error: duplicate values found in : BUS -> MOTORCYCLE

 

In the above implementation, “BUS” and “MOTORCYCLE” have the same value “3”. As a result, the @unique decorator raises a ValueError with a message indicating that duplicate values have been found. Neither can you use the same key more than once nor can you assign the same value to different members. In this manner, it helps prevent duplicate values for multiple enumeration members.

 

 

The partial decorator is a powerful tool that is used to create partial functions. Partial functions allow you to pre-set some of the arguments of the original function and generate a new function with those arguments already filled in.

 

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)

 

Output:

 

In the above implementation, we have a function “power” which accepts two arguments “base” and “exponent” and returns the result of the base raised to the power of exponent. We have created a partial function named “square” using the original function in which the exponent is pre-set to 2. In this way, we can extend the functionality of original functions using a partial decorator.

 

 

The @singledisptach decorator is used to create generic functions. It allows you to define different implementations of functions with the same name but different argument types. It is particularly useful when you want your code to behave differently for different data types.

 

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])     

 

Output:

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 developed the generic function display_info() using the @singledisptach decorator and then registered its implementation for int, float, string, and list separately. The output shows the working of display_info() for separate data types.

 

 

The @classmethod is a decorator used to define class methods within the class. Class methods are bound to the class rather than the object of the class. The primary distinction between static methods and class methods lies in their interaction with the class state. Class methods have access to and can modify the class state, whereas static methods can not access the class state 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}")

 

Output:

 

Class method called. Total students now: 3
Total students from student 1: 3
Total students from student 2: 3

 

In the above implementation, the Student class has total_students as a class variable. The @classmethod decorator is used to define the increment_total_students() class method to increment the total_students variable. Whenever we create an instance of the Student class, the total number of students is incremented by one. We created two instances of the class and then used the class method to modify the total_students variable to 3, which is also reflected by the instances of the class.

 

 

The @staticmethod decorator is used to define static methods within a class. Static methods are the methods that can be called without creating an instance of the class. Static methods are often used when they don’t 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)

 

Output:

 

In the above implementation, we have used @staticmethod to define a static method add() for the class “MathOperations”. 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”. This way static methods can be generated to perform utility functions that do not require the state of an instance. 

 

 

The @property decorator is used to define the getter methods for the class attribute. The getter methods are the methods that return the value of an attribute. These methods are used for data encapsulation which 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)  

 

Output:

 

In the above implementation, the class “Circle” has an attribute “radius”. We have used @property to set up the getter methods for the radius as well as the area. It provides a clean and consistent interface for the users of the class to access these attributes. 

 

 

This article highlights some of the most versatile and functional decorators that you can use to make your code more flexible and readable. These decorators let you extend the functionalities 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 neat and work smoothly.
 
 

Kanwal Mehreen is an aspiring software developer with a keen interest in data science and applications of AI in medicine. Kanwal was selected as the Google Generation Scholar 2022 for the APAC region. Kanwal loves to share technical knowledge by writing articles on trending topics, and is passionate about improving the representation of women in tech industry.