Making a Python User-Defined Class Sortable and Hashable

Sorting Hashing Classes Featured

Python has a very efficient mechanism that lets us define user-defined classes, which are nothing but data types that we create as per our requirements.

As developers, we often try to make our user-defined data types as close as possible to the system-defined ones concerning features and characteristics to maintain efficiency. Several data types that are system defined are often hashable and sortable, and implementing this characteristic in our user-defined one could be a tricky challenge to figure out.

In this article, we will be making a Python user-defined class sortable and hashable and understanding all the terms and concepts related to it.

Importance of Sorting and Hashing in Python

Sorting and Hashing are two very important concepts when talking in terms of computer science.

In terms of Python, we will understand these concepts.

Sorting

To manage and organize data effectively, sorting becomes important, as it is a fundamental function in Python. By helping in fast searching with algorithms like binary search, it allows components to be ordered in an accurate order.

Information is displayed in a structured and presentable fashion, which improves the user experience. Sorting is frequently required before using other methods such as determining the median or spotting duplicates.

Hashing

Retrieval in constant time is achieved as hashing is an important task that gives access to data through hash tables. Consistency is checked, and changes or corruptions are detected to safeguard data integrity. As it boosts data retrieval performance, hashing is used in a variety of indexing and caching algorithms

Increased overall efficiency and saved storage space are achieved as it helps in data deduplication by assisting in the identification and elimination of duplicate data. Hashable classes can be used as keys in dictionaries, allowing for quick data access and retrieval.

Making Custom Classes Sortable and Hashable: Benefits

Making these classes sortable and hashable allows us to fully utilize Python’s built-in sorting and hashing capabilities, as well as use them in bespoke data structures.

Sorting Benefits

The advantages of sorting custom classes in Python are numerous.

In the first place, it enables us to specify unique sorting criteria, customizing the order of objects to meet certain application requirements. This increased flexibility makes it possible to sort using many attributes or intricate calculations that built-in sorting methods might not be able to handle.

Custom sortable classes also become interoperable with other sorting-dependent Python modules and procedures. Since they are easily able to be incorporated into current sorting algorithms, they are more adaptable and can be reused. Unique data structures that properly meet application needs can be created, complex algorithms can be implemented, and data processing can be optimized because of all the above benefits.

Hashing Benefits

The advantages of hashing for Python’s custom classes are numerous and strong. The retrieval and lookup processes are done in constant time because of hash tables, which provide effective data retrieval.

Data integrity is guaranteed as long as any alterations or corruption in the dataset are identified. The data retrieval process is enhanced as it makes indexing and caching technologies easier to use. Improving overall effectiveness and reducing storage requirements through hashing also helps with removing duplicate data.

Custom classes can be used as keys in dictionaries to enable quick access to associated values when they are hashable. To enhance program speed and optimize function calls, we can use methods like caching and memoization. Custom key functions also make it simple to incorporate sophisticated data structures and set operations.

Understanding Sorting and Hashing in Python

Sorting and Hashing in the Context of Python

Sorting 

Placing items in a particular order is part of the sorting process. Sorting functions like sorted() and list are built into Python. Sort lists, tuples, and other iterable objects with the sort() method. 

Let us see what a basic program that involves sorting looks like with the help of an example:

nums = [6,1,5,2,4,3,9,8,7]

sorted_nums = sorted(nums)

print("Unsorted Numbers: ", nums)
print("Sorted Numbers: ", sorted_nums)

The above code uses the default sorting function, i.e., ‘sorted(), which is already present in Python.

Simple Sorting Example
Simple Sorting Example

Hashing

Using a hash function, data, also known as keys, is transformed into a fixed-size representation, which is a hash value. Python dictionaries hold key-value pairs using hashing for quick lookups.

Let us see how a basic program that involves hashing looks with the help of an example:

student = {'name': 'Raj', 'age': 20, 'city': 'Pune'}
print(hash('name'))
Simple Hashing Example 1
Simple Hashing Example

How Built-in Data Types like Lists and Dictionaries are Sortable and Hashable

Lists

One of the most flexible and popular data types in Python is lists. They can be naturally sorted using the ‘list.sort()’ function or the ‘sorted()’ function. The items are sorted according to their natural order, and their default comparison operators are used to compare them.

Since Python cannot detect the relative ordering of elements of various types, sorting may result in a TypeError. It is crucial to remember that lists cannot be hashed in Python. This is because lists are mutable, meaning their elements can change, and mutable objects’ hash values can change over time.

As a result, lists cannot be used as elements in sets or as keys in dictionaries because both applications demand hashable objects. You can convert lists to tuples, which are immutable and consequently hashable, and use tuples as dictionary keys because they are hashable.

To learn more Lists, check out the linked resource.

Dictionaries

Offering a strong method of storing key-value pairs, Python has another important data type, which is a dictionary. Although dictionaries are naturally hashable, they are not automatically sortable.

This is because dictionaries lack a natural order for their elements. After all, they are mappings rather than sequences. Use the sorted() function with a custom key argument to sort a dictionary according to its keys or values while keeping the order of entry in mind.

It’s important to understand that the sorted result will not be a dictionary but rather a list of key-value pairs. A dictionary’s keys must be hashable, or immutable, to guarantee a constant hash value. As dictionary keys, immutable objects like texts, integers, or tuples are frequently employed.

Mutable objects cannot be hashed, so if you try to use one as a key, such as a list, a TypeError will be raised.

Limitations of using default sorting and hashing for custom classes.

Before getting into implementation, let us figure out what the possible problems and limitations of using default sorting and hashing for custom classes are. It’s possible to run into restrictions and unexpected behavior when using Python’s default sorting and hashing algorithms for custom classes.

Custom classes don’t naturally sort in a certain order, so when built-in functions are used to attempt to do so, a TypeError is raised. Additionally, the equality comparison is by default based on object identity rather than attribute values, which may lead to surprising outcomes. To get around these restrictions, the custom class description must have the right special methods, assuring proper sorting and hashing capabilities.

Magic Methods in Python

Python has certain functions that perform special operations and are known as magic Methods. To understand what these functions are? How they are used? And why are they used? Let’s start examining them. Double underscores are used at the start and end of the names of Magic Methods to identify them.

Being called automatically in certain situations, these methods give a mechanism to modify an object’s behavior in Python. Developers can specify how objects interact with operators and built-in functions, as they allow user-defined classes to smoothly copy the behavior of built-in types.

Why Use Magic Methods?

The ability of magic methods to modify Python objects default functionality is what gives them their strength. For instance, we can specify how objects should act when the + operator is applied to them by using the __add__ method. Once this function is defined, we can allow the addition of instances to our custom classes.

The __init__ method is used to initialize the attributes when an object is created. By customizing this function, we can give initial values for the attributes of our objects

Common Magic Methods for Sorting and Hashing

After understanding the concept, let us jump to some common magic methods and understand their implementation using some simple examples

‘__lt__’

Customizing the less-than comparison between objects is done using this technique.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __lt__(self, other):
        return self.age < other.age

p1 = Person('Raj', 15)
p2 = Person('Rahul', 20)
print(p1 < p2) 

I simply made a class called “Person” and gave it two data members, “name” and “age,” which are initialized using the “__init__” function.

If you want to read more about how Python classes work, you can read this article.

Then on line 6, I used the ‘__lt__’ method, which takes one parameter for comparison.

The function basically compares two parameters and returns True if the first is less than the second; otherwise, it returns false. The output example can be seen in the picture below.

Lt Magic Method Example
__lt__ Magic Method Example

‘__eq__’

This approach enables the equality comparison between things to be tailored. It is essential for both assessing whether two things are equal and for hashability. 

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return self.age == other.age

p1 = Person('Raj', 15)
p2 = Person('Rahul', 20)
print(p1 == p2) 

In the above code, I used the same class that we created in the previous example, and this time on line 6, I used the ‘__eq__’ magic method.

This function compares two objects, but instead of returning false if the elements are not equal, it returns true if they are. The output example is displayed in the picture below:

Eq Magic Method Example
__eq__ Magic Method Example

‘__hash__’

Enabling it to be used as a dictionary key or a set element, this technique is used to determine an object’s hash value.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __hash__(self):
        return hash((self.name, self.age))

p1 = Person('Raj', 15)
p2 = Person('Raj', 15)

people = {p1, p2}
print(len(people)) 

I have implemented the __hash__ magic method again in the same class that we created earlier for the previous few examples.

I used the __hash__ method on line 6, which will perform it operation as mentioned in the definition. You can see the output in the image below:

Hash Magic Method
__hash__ Magic Method Example

So overall, with the help of the magic method, we have overloaded certain operators to perform some special tasks based on our class and its requirements.

Implementing Sorting in a User-Defined Class

For a user-defined class to perform custom sorting behavior, the ‘ __lt__’  and ‘__eq__’ methods are crucial.

In this section, we will extensively see how we can use the magic methods which we saw in the previous section to satisfy our need, which is incorporating a sorting behaviour in our user defined class

How to Override These Methods to Define the Sorting Order for Objects

We must override the ‘__lt__’ method in order to enable sorting for a user-defined class. To determine the order of the items, this method should compare the relevant properties of the objects.

It is frequently advised to implement the ‘__eq__’ method as well for more reliable behavior. We will be using the same class that we’ve been using for all the previous examples,

This time we would include both the required magic methods, as you can see below:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __lt__(self, other):
        return self.age < other.age

    def __eq__(self, other):
        return self.age == other.age
Basic Class With Magic Methods Example
Basic Class With Magic Methods Example

We override the ‘<’ and ‘==’ symbol to satisfy our requirement.

Sorting Objects based on Different Criteria

Let us jump to an example to get a better understanding of this concept while we are analyzing the code.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __lt__(self, other):
        return self.age < other.age

    def __eq__(self, other):
        return self.age == other.age


people = [Person('Rahul', 15), Person('Rajesh', 20), Person('Mohun', 22), Person('Raj', 34)]

sorted_people = sorted(people, key = lambda x:len(x.name))

print("Original List: ", [j.name for j in people])
print("Sorted List: ",[i.name for i in sorted_people])

In the above example, we arrange the elements according to how long their names are. We can define multiple sorting orders for the items based on the criteria that best fit our needs by overriding the __lt__ method and giving a custom key parameter to the sorted() function.

Lastly, I have just printed out the names of the objects using a simple list comprehension mechanism.

Implemtation Sort User Defined Class
Implementation of Sorting in User Defined Class

Achieving Hashability in a User-Defined Class

Python has a special function called hash to find an object’s hash value. Hashing is necessary to produce hashable objects, which can be used as parts of sets and as keys in dictionaries. Fast data retrieval and lookup procedures are made possible by the hash value, an integer that serves as an object’s identification.

Need for Creating a Proper Hash Implementation

To produce an appropriate __hash__() implementation for hashability, this method must follow certain guidelines. First of all, it must be deterministic, delivering the same hash value for a particular item across the course of that object’s existence.

When used as a dictionary key or a set element, it guarantees the object’s integrity. Any modifications to the characteristics should have no impact on the hash value, keeping it in its initial state.

Finally, to ensure that objects deemed equal have the same hash value, the __hash__() function should be in line with the __eq__() method. A Python hashable custom class that satisfies these requirements will be stable and dependable.

Implementing __hash__ for Different Class Structure

We will see how we can implement __hash__ for different class structures, as classes can be of different types based on their members and their nature.

Immutable Classes

You may frequently rely on the built-in hash function for tuples, which automatically produces the hash based on the content of the elements, for classes with immutable characteristics, such as tuples or namedtuples.

You can see the practical example in the code example below:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))

Mutable Classes with Immutable Attributes

You can use a hash function based on the immutable attributes for classes having changeable properties but whose hash value only depends on the immutable attributes.

You can understand better by trying the example given below:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return self.name == other.name and self.age == other.age

    def __hash__(self):
        return hash((self.name, self.age))

You can make your user-defined class hashable and use it effectively as a key in dictionaries or an element in sets, allowing for efficient data retrieval and manipulation in Python, by correctly implementing the __hash__() method and adhering to the conditions for hashability.

Use cases and real-world examples

Practical Scenarios where Sortable and Hashable Classes are Beneficial 

For effective organization and manipulation of data, sortable and hashable classes are very useful in many situations. Improving performance when working with massive datasets, these classes speed up record searching and sorting in database operations.

Hashable classes are advantageous for caching and memoization because they enable storing the outcomes of expensive function calls depending on input parameters, lowering computation time for frequent operations. Essential for operations like topological sorting and shortest path finding, sortable classes in graph algorithms allow for the custom ordering of graph nodes or edges.

Providing data analysis and manipulation, hashable classes also make it easier and more efficient to design complicated data structures like nested dictionaries or sets.

Real world Examples to Illustrate the Versatility of Custom Classes

A custom class representing geographic coordinates can be made sortable based on longitude or latitude in geospatial data applications, facilitating geographic data processing and visualization. A hashable class for product items can act as the keys in a dictionary for inventory management, enabling quick access to and updating of product data.

Helping in inventory management and optimization, these classes can be sorted according to attributes like price or availability. Sortable classes that represent jobs with deadlines in task scheduling systems enable effective task prioritization. To keep separate sets of work organized and prevent the scheduling of duplicate tasks, hashable classes can be utilized.

Conclusion

Summary

So here we are, finally reaching the end of this intense journey of understanding how we can make custom-made or user-defined classes hashable and sortable. We started off by covering all the concepts and pointers needed to understand what we were about to do. Then, after gaining knowledge, it was time to put that knowledge into practical use, and that is what we did.

We left no stone unturned and finally figured out that “Yes’ it is possible to make the user-defined classes hashable and sortable. Towards the end, we covered some practical use cases and relevant real world applications.

Reference

Stackoverflow Query