Welcome, guest | Sign In | My Account | Store | Cart

The catch is that the unhashable objects aren't actually stored in the mapping. Only their IDs are. Thus you must also store the actual objects somewhere else.

Python, 24 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try:
    from collections.abc import MutableMapping
except ImportError:
    from collections import MutableMapping


class IDKeyedMapping(MutableMapping, dict):
    '''A dict that can take mutable objects as keys.'''
    def __len__(self):
        return dict.__len__(self)
    def __iter__(self):
        return dict.__iter__(self)
    def __contains__(self, key):
        return dict.__contains__(self, id(key))
    def __getitem__(self, key):
        return dict.__getitem__(self, id(key))
    def __setitem__(self, key, value):
        dict.__setitem__(self, id(key), value)
    def __delitem__(self, key):
        dict.__delitem__(self, id(key))
    def values(self):
        return dict.values(self)
    def items(self):
        return dict.items(self)

Alterately, implement __hash__() on your mutable object's class:

class MyType:  # Python 3 implicit base: object
    def __hash__(self):
        return id(self)

If you want to use a type you don't control, subclass it:

class NewType(original_type):
    def __hash__(self):
        return id(self)

If that doesn't work (as is the case with a very few built-in types) or you have an existing object, you could write a simple wrapper class that also implements __hash__().

2 comments

Steven D'Aprano 9 years, 6 months ago  # | flag

This is very ingenious, but I wouldn't want to touch it with a ten foot pole for production code. Trying to debug a problem with code using this class would be a sheer nightmare, and the risk of surprising behaviour is way too high.

Surprise #1: equal keys don't work.

>>> mapping = IDKeyedMapping()
>>> x = [1, 2, 3]
>>> mapping[x] = 42
>>> mapping.get([1, 2, 3], 'not found')
'not found'

Surprise #2: mapping.keys() and items() don't give you the keys you care about.

>>> x in mapping
True
>>> x in list(mapping.keys())
False

Surprise #3: if you delete the real key, you can get a match by accident with the wrong key.

>>> del x
>>> y = []
>>> mapping[y]
42

(At least in CPython, in Jython IDs are guaranteed to never be reused.)

There may be other surprises I haven't thought of.

Eric Snow (author) 9 years, 6 months ago  # | flag

Oh yeah. I wouldn't use it in production either. :) I expect you could iron out the wrinkles, but there are better solutions.