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.
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.