Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the dictionary key being converted to an inherited class type?

My code looks something like this:

class SomeClass(str):
    pass

some_dict = {'s':42}

>>> type(some_dict.keys()[0])
str
>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict # expected: Two different keys-value pairs
{'s': 40}
>>> type(some_dict.keys()[0])
str

Why did Python convert the object s to the string "s" while updating the dictionary some_dict?

like image 512
Satwik Avatar asked Jan 07 '18 08:01

Satwik


1 Answers

Whilst the hash value is related, it is not the main factor. It is equality that is more important here. That is, objects may have the same hash value and not be equal, but equal objects must have the same hash value (though this is not strictly enforced). Otherwise you will end up with some strange bugs when using dict and set.

Since you have not defined the __eq__ method on SomeClass you inherit the one on str. Python's builtins are built to allow subclassing, so __eq__ returns true, if the object would otherwise be equal were it not for them having different types. eg. 's' == SomeClass('s') is true. Thus it is right and proper that 's' and SomeClass('s') are equivalent as keys to a dictionary.

To get the behaviour you want you must redefine the __eq__ dunder method to take into account type. However, when you define a custom equals, python stops giving you an automatic __hash__ dunder method, and you must redefine it as well. But in this case we can just reuse str.__hash__.

class SomeClass(str):
    def __eq__(self, other):
        return (
            type(self) is SomeClass
            and type(other) is SomeClass
            and super().__eq__(other)
        )

    __hash__ = str.__hash__

d = {'s': 1}
d[SomeClass('s')] = 2

assert len(d) == 2
print(d)

prints: {'s': 2, 's': 1}

like image 127
Dunes Avatar answered Nov 16 '22 04:11

Dunes