tomasz
tomasz

Reputation: 180

Hashable subclasses of mutable classes in Python

I want to build a subclass of a class which is basically a wrapper around a list/dictionary, and I want to use instances of this class as keys. In order to have easy access to the list/dictionary methods, I figured I can just subclass them, but then I lose hashability.

I understand that this is incompatible with the __eq__ of the mutable classes (and I don't need it), so I came up with the following solution.

class Foo(list):
    __hash__ = object.__hash__
    __eq__ = object.__eq__

Though I doubt this will come up in my specific applications, I guess this might cause problems if another class (with its own __hash__) would appear between Foo and list in the MRO of a subclass.

  1. Is there a more Pythonic way of doing this? I could just add a list/dictionary as an attribute and then simply use the attribute (which would make code elsewhere more cumbersome) or the list/dictionary methods I need (but that'd be much more messy if I need more than a couple of methods).
  2. Is there some pattern/recipe that would allow me to do this sort of thing in a cooperative setting (in other words, how should I change the code if I do in fact anticipate another class between Foo and list in MRO)?

Upvotes: 0

Views: 105

Answers (1)

FloLie
FloLie

Reputation: 1841

I decided a provide an answer based on my comment, with a lot if IFs

As stated, I would rather change the implementation of the dict, to take a list as a key. This however does come with problems, that as others mentioned, is the reason why lists SHOULDN'T be keys.

A little explanation:

When looking at a mutable variable, it comes with two properties, that it can be compared by:

  1. Its identity
  2. Its content

Identity When a variable is created such as a=[1,2,3] it is allocated a memory position which can be called with id(a). This id identifies a even after a is modified. However with a second variable b=[1,2,3] when comparing ids we get id(a) == id(b) = False

Content We can also look at the content of a variable in a very simple way by converting it to a String, so with above example str(a) == str(b) = True. However if we do

a = [1,2,3]
str_a = str(a)
a.append(4)
str_new_a = str(a)

we get str_a == str_new_a = False, which is problematic in the case of application as a key in a dict, because at the moment of saving the value, we take a 'snapshot' of a and later if we lookup by a after it changed, it wont match.

Therefore you will have to decide, if in your case content or identity is the relevant lookup criterion.

An example implementation with hashableKey1 as an ID match and hashableKey2 as a content mach is provided below.

from collections import Hashable

class CoolDict(dict):
    hashableKeyType = 1
    def __setitem__(self, name, value):
        print(name, value)
        if isinstance(name, Hashable):
            super().__setitem__(name, value)
        else:
            super(CoolDict, self).__setitem__(self.hashableKey(name), value)
    def __getitem__(self, name):
        if isinstance(name, Hashable):
            return super().__getitem__(name)     
        else:
            return super().__getitem__(self.hashableKey(name))   
    
    def hashableKey(self, name):
        if self.hashableKeyType == 1:
            return self.hashableKey1(name)
        elif self.hashableKeyType == 2:
            return self.hashableKey2(name)
    def hashableKey1(self, name):
        return id(name)
            
    def hashableKey2(self, name):
        return str(name)
    
coolDict  = CoolDict()

### hashableKey1:

#Working
key = [1,2,3]
coolDict[key] = 123
print(coolDict[key])

#Not Working
coolDict[[2,3,4]] = 234
print(coolDict[[2,3,4]])

### hashableKey2:
coolDict.hashableKeyType = 2

#Working
coolDict[[2,3,4]] = 234
print(coolDict[[2,3,4]])


#Not Working
key = [1,2,3]
coolDict[key] = 123
key.append(4)
print(key)
print(coolDict[key])

Upvotes: 1

Related Questions