kdb
kdb

Reputation: 4416

Python: Create __hash__ and comparison functions compactly?

As pointed out in __key__ parameter for classes in Python, Python3.x has functools.total_ordering, but it still requires manually implementing __lt__, __hash__ and __eq__ manually.

In many cases what I need thus boils down to the still repetitive pattern

@functools.total_ordering
class X:
    ...
    _key = lambda self: (self.field1, self.field2)
    __lt__ = lambda self, other: self._key() < other._key()
    __eq__ = lambda self, other: self._key == other._key
    __hash__ = lambda self: hash(self._key())

The motivation for the _key function is the key parameter used in utility funcitons like sorted, which behaves roughly like this -- calculate a tuple from the object, and then compare the tuples.

Is there some feature of python, that allows defining a class-level function, from which all comparison function and a __hash__ function are derived, similarly to what I do manually?

Related question

There is __key__ parameter for classes in Python, but it considers only comparison operators. The __hash__ function is never considered. As a result the question looks similar, but treats only a subset of my question.

Upvotes: 0

Views: 205

Answers (2)

buran
buran

Reputation: 14233

Look at dataclasses module from Standard Library:

This module provides a decorator and functions for automatically adding generated special methods such as __init__() and __repr__() to user-defined classes. It was originally described in PEP 557.

this include also options for generating comparison and hash special methods.

from dataclasses import dataclass

@dataclass(order=True)
class Foo:
    field1: int
    field2: int


spam = Foo(1, 2)
eggs = Foo(2, 3)
foo = Foo(2, 1)
bar = Foo(2, 1)

print(spam > eggs)
print(eggs == foo)
print(bar < eggs)
print(dir(Foo))

output

False
False
True
['__annotations__', '__class__', '__dataclass_fields__',
 '__dataclass_params__', '__delattr__', '__dict__', '__dir__',
 '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
 '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
 '__lt__', '__module__', '__ne__', '__new__', '__reduce__', 
 '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
 '__subclasshook__', '__weakref__']

We don't have full information about your class, so it's difficult to tell if it will solve your problem.

Upvotes: 2

MisterMiyagi
MisterMiyagi

Reputation: 50116

You can implement your own class-decorator to automatically add __hash__, __eq__ and __lt__ in terms of a __key__ (or other known method name).

def key_hash(self):
    return hash(self.__key__())

def key_lt(self, other):
    return self.__key__() < other.__key__()

def key_eq(self, other):
    return self.__key__() == other.__key__()


def keyed(cls):
    for op_name, op_func in (("__hash__", key_hash), ("__lt__", key_lt), ("__eq__", key_eq)):
        setattr(cls, op_name, op_func)
    return cls

This can be applied just like total_ordering and also combined with it:

@functools.total_ordering
@keyed
class X:
    def __init__(self, field1, field2):
        self.field1, self.field2 = field1, field2

    def __key__(self): return self.field1, self.field2

If you always want keyed and functools.total_ordering, they can be merged into one decorator as well.

def total_keyed(cls):
    for op_name, op_func in (("__hash__", key_hash), ("__lt__", key_lt), ("__eq__", key_eq)):
        setattr(cls, op_name, op_func)
    return functools.total_ordering(cls)

Upvotes: 1

Related Questions