Adirio
Adirio

Reputation: 5286

Integer subclass hash

I have a couple int child classes that represent different ranges. Some of their behavior is shared in an abstract class and I want them to be comparable among themselves and with plain integers but not with other different ranges. Hashing stays the same as integers, so there will be collisions, but I thought that wasn't an issue as an equality check will be performed to solve the hash collision.

from abc import ABC, abstractmethod


class Range(int, ABC):
    @classmethod
    @property
    @abstractmethod
    def min(self) -> int:
        raise NotImplementedError

    @classmethod
    @property
    @abstractmethod
    def max(self) -> int:
        raise NotImplementedError

    def __init__(self, *args, **kwargs):
        super().__init__()
        # Check the lower and upper bounds
        if not (self.min <= self <= self.max):
            raise ValueError

    def __eq__(self, other):
        # If they are a range but not the same one just return False
        if isinstance(other, Range) and not isinstance(other, self.__class__):
            return False
        return NotImplemented

    def __ne__(self, other):
        # If they are a range but not the same one just return True
        if isinstance(other, Range) and not isinstance(other, self.__class__):
            return True
        return NotImplemented

    def _check_other(self, other):
        if isinstance(other, Range) and not isinstance(other, self.__class__):
            raise NotImplementedError

    def __lt__(self, other) -> bool:
        self._check_other(other)
        return int(self) < int(other)

    def __le__(self, other) -> bool:
        self._check_other(other)
        return int(self) <= int(other)

    def __gt__(self, other) -> bool:
        self._check_other(other)
        return int(self) > int(other)

    def __ge__(self, other) -> bool:
        self._check_other(other)
        return int(self) >= int(other)

    __hash__ = int.__hash__


class Range20(Range):
    min = 1
    max = 20


class Range100(Range):
    min = 1
    max = 100


m = {
    Range20(1): -1,
    Range20(2): -2,
    Range100(1): 1,
    Range100(2): 2,
}

assert Range20(1) == 1, "Range20(1) != 1"
assert Range20(1) != 2, "Range20(1) == 2"
assert Range20(1) == Range20(1), "Range20(1) != Range20(1)"
assert Range20(1) != Range20(2), "Range20(1) == Range20(2)"
assert Range20(1) != Range100(1), "Range20(1) == Range100(1)"
assert not (Range20(1) == Range100(1)), "Range20(1) == Range100(1) bis"
assert Range20(2) > 1, "Range20(2) <= 1"
assert Range20(2) >= 2, "Range20(2) < 2"
assert Range20(2) < 3, "Range20(2) >= 3"
assert Range20(2) <= 2, "Range20(2) > 2"
assert Range20(2) > Range20(1), "Range20(2) <= Range20(1)"
assert Range20(2) >= Range20(2), "Range20(2) < Range20(2)"
assert Range20(2) < Range20(3), "Range20(2) >= Range20(3)"
assert Range20(2) <= Range20(2), "Range20(2) > Range20(2)"
assert hash(Range20(1)) == hash(1), "hash(Range20(1)) != hash(1)"
assert hash(Range20(1)) == hash(Range100(1)), "hash(Range20(1)) != hash(Range100(1))"
assert Range20(1) in m, "Range20(1) not in m"
assert Range100(1) in m, "Range100(1) not in m"
assert 1 not in m, "1 in m"

All assertions should pass but some aren't passing right now. The last assertion may be hard to achieve as they are considered to be equal by __eq__ so not passing that one would be fine, but the previous two need to pass so that m[Range20(1)] == -1 and m[Range100(1)] == 1 (i.e., I can access the values in the map).

Edit: added comparisson methods and assertions but the result is still the same Edit 2: added additional assertions

Upvotes: 0

Views: 89

Answers (2)

Adirio
Adirio

Reputation: 5286

@MoeNeuron guided me in the right direction, by adding some additional asserts I discovered that assert Range20(1) == Range20(1) was failing. I thought that by returning NotImplemented the int method would be able to handle those cases but it seems it doesn't. The solution is to use the following two modified methods:

    def __eq__(self, other):
        if isinstance(other, Range):
            if not isinstance(other, self.__class__):
                # If they are a range element but not from the same range
                return False
            # If they are a range element from the same range
            return int(self) == int(other)
        # If they are not a range element delegate to int
        return NotImplemented

    def __ne__(self, other):
        if isinstance(other, Range):
            if not isinstance(other, self.__class__):
                # If they are a range element but not from the same range
                return True
            # If they are a range element from the same range
            return int(self) != int(other)
        # If they are not a range element delegate to int
        return NotImplemented

With these modified methods, only the last assertion fails, which is acceptable as I said in the question.

Upvotes: 0

MoeNeuron
MoeNeuron

Reputation: 78

Changing this part here works with me (except assertion 17 fails -- which it doesn't actually align with i.e. the previous equality tests, unless you are wishing for something else I missed in your question):

def __eq__(self, other):
    # If they are a range but not the same one just return False
    if isinstance(other, Range) and not isinstance(other, self.__class__):
        return False
    elif isinstance(other, Range) and isinstance(other, self.__class__):
        return True
    return NotImplemented

def __ne__(self, other):
    # If they are a range but not the same one just return True
    if isinstance(other, Range) and not isinstance(other, self.__class__):
        return True
    elif isinstance(other, Range) and isinstance(other, self.__class__):
        return False

    return NotImplemented

Upvotes: 1

Related Questions