two sets were not equal, but are equal after casting to list and cast back to set

Viewed 55

Does anyone got this weird outcome before?

>>> a == b
False
>>> list(sorted(a)) == list(sorted(b))
True
>>> set(list(a)) == set(list(b))
True

Where a and b above are set containing custom class instances.

This custom class inherited from MutableMapping with both __eq__() and __hash__() implemented as follows:

def to_json(self) -> dict:
    # Code returning the data of this class

def __eq__(self, other):
    if isinstance(other, Model):
        return self.to_json() == other.to_json() and type(self) == type(other)
    elif isinstance(other, MutableMapping):
        return self.to_json() == other
    else:
        return False

def __hash__(self):
    d = self.to_json()

    hash_list = []

    for k, v in d.items():
        if isinstance(v, list):
            v = tuple(v)
        elif isinstance(v, dict):
            v = tuple(v.items())
        elif isinstance(v, Model):
            v = hash(v)

        hash_list.append((k, v))

    return hash(tuple(hash_list))

I also test the hash code of these elements, it turns out it is the same. Below is my script to test:

>>> [hash(m) for m in a]
[-1696378346402890742, 3465342798672228497, 5576155172607749152]
>>> [hash(m) for m in b]
[-1696378346402890742, 3465342798672228497, 5576155172607749152]

I've found that there's some work to do with in, but I don't know what should I implement. I also don't know why this is its behavior.

>>> [(m in b) for m in a]
[False, False, False]
>>> [(m in b) for m in set(list(a))]
[False, False, False]
>>> [(m in set(list(b))) for m in a]
[True, True, True]
>>> [(m in set(list(b))) for m in set(list(b))]
[True, True, True]

Any fix that could potentially avoid/correct this weird behavior and the reason why this is how it works would be appreciated. Thanks!

2 Answers

I don't have a lot of experience with sets, but a == b where a and b are sets with the same elements, returns false because sets are unordered. This means the order of the elements is randomized every time you use the set. The == sign is trying to compare elements in the sets but in random order, unlike lists.

From the python documention: Being an unordered collection, sets do not record element position or order of insertion. Accordingly, sets do not support indexing, slicing, or other sequence-like behavior.

When you use [(m in b) for m in a] The compiler reads (m in b) first so it translates to:

[False for m in a] and thats why it gives you this result.

I assume you meant to check if every element in set b is also in set a.

For that purpose, you can use [True if (m in a) else False for m in b].

I don't know much about hash codes with sets, but I think it is a specific interaction with sets, so try to check the documentation maybe.

I think in the case of a list, the outcome is based on the result of __eq__ only, whereas in the case of sets, it depends on __hash__ as well. In a minimal example like the following, you can see that lists of different objects with the same __eq__-value are evaluated to be the same, whereas sets are not. If you define a __hash__-function that depends on the value only (not on the instance), also the corresponding sets evaluate to equal.

class EA2:
    def __init__(self, id):
        self.id = id

    def __eq__(self, other):
        if type(self) == type(other):
            return self.id == other.id
        else:
            return False

    def __hash__(self):
        #a = hash((id(self), self.id))
        a = self.id
        #a = randint(1,1000)
        print("hash= ", a)
        return a

print({EA2(1), EA2(2)} == {EA2(1), EA2(2)})
print(list({EA2(1), EA2(2)}) == list({EA2(1), EA2(2)}))
print(EA2(1) == EA2(1))

If your comparisons evaluate to False despite equal hashes, maybe there is a different implementation of the __hash__-function around (due to subclassing)?

Related