Unordered list as dict key

Viewed 606

I want to be able to do something like:

foo = Counter(['bar', 'shoo', 'bar'])
tmp = {}
tmp[foo] = 5

In other words, is there a hashable equivalent for Counter? Note that I can't use frozenset since I have repeated elements that I want to keep in the key.

Edit: In my actual application, the objects in foo may not be comparable with each other so the list cannot be sorted.

5 Answers

What you seem to require is a way to use unordered pairs of key-amount as keys. A frozenset is probably the way to go, although you will have to create it out of the items of the Counter and not its keys.

foo = Counter(['bar', 'shoo', 'bar'])
tmp = {}
tmp[frozenset(foo.items())] = 5

# tmp: {frozenset({('bar', 2), ('shoo', 1)}): 5}

If this is satisfying, you could implement this transformation by defining you own mapping type like so:

from collections import Counter

class CounterDict:
    def __init__(self):
        self.data = {}

    def __setitem__(self, key, value):
        if isinstance(key, Counter):
            self.data[frozenset(key.items())] = value
        else:
            raise TypeError

    def __getitem__(self, key):
        if isinstance(key, Counter):
            return self.data[frozenset(key.items())]
        else:
            raise TypeError

foo = Counter(['bar', 'shoo', 'bar'])
tmp = CounterDict()
tmp[foo] = 42
tmp[foo] # 42

You could make this implementation richer by making CounterDict a subclass of collections.UserDict.

There are a couple of things you could do. One is to sort the list and convert the result to a tuple. That will work fine for small lists.

If you have large lists (with duplicates), you could convert it into a frozenset whose elements are tuples which are (word, count) pairs. So for the example, if your list is ['bar', 'shoo', 'bar'], you would have frozenset({('bar', 2), ('shoo', 1)}).

You could construct this by using Counter, or else just construct a dict of word: count entries, then convert the entries to tuples and construct a frozenset from them.

Both of these would be hashable, and support duplicate elements in the original list.

Using the string representation of the Counter?

foo = Counter(['bar', 'shoo', 'bar'])
tmp = {}

tmp[str(foo)] = 5

The problem is that Counter is a mutable object, and so is dict. If you are OK with converting it to an immutable one, then convert it to tuple. For example,

dict_as_tuple = tuple(sorted(foo.items()))
foo[dict_as_tuple] = 5

You can do backward conversion to dictionary by

d = {k: v for k, v in dict_as_tuple}

I agree with @roganjosh that it is a Devil's advocate advice, but that's the closest thing to a solution I may think of.

Mapping keys need to be hashable. We can create a HashableCounter subclass of Counter that provides a __hash__() method so instances can be used as keys.

Hash values must be equal if the objects are equal. The hash value must be independent of the order items are added to the HashableCounter.

The objects added to the HashableCounter are hashable. Define a canonical order of the keys by sorting them by their hashes. Then take a hash of a tuple of the ordered (key, value) pairs as the hash for the HashableCounter.

Caveats:

  • If a HashableCounter is updated or changed, the hash value will change even though it's the same object (same HashableCounter instance).
  • This may be seen as an abuse of the concept of a hash.
  • A hash value may be different for different runs of the program, different Python versions or implementations, different OSes, etc.
  • It's not an expected usage - document it.

Here's the code:

from collections import Counter

class HashableCounter(Counter):
    def __hash__(self):
        items = sorted(self.items(), key=lambda t:hash(t[0]))
        return hash(tuple(items))

foo1 = HashableCounter(['bar', 'shoo', 'bar'])
print(f"foo1 = {foo1}, hash = {hash(foo1)}")

# different order, same hash
foo2 = HashableCounter(['shoo', 'bar', 'bar'])  
print(f"foo2 = {foo2}, hash = {hash(foo2)}")

#use foo1 as a key
tmp = {}
tmp[foo1] = 5
print(f"tmp = {tmp}")

# update tmp using foo2 as the key
tmp[foo2] *= 3
print(f"tmp = {tmp}")

print(foo1 == foo2)

Output:

foo1 = HashableCounter({'bar': 2, 'shoo': 1}), hash = 8916343845449291478
foo2 = HashableCounter({'bar': 2, 'shoo': 1}), hash = 8916343845449291478
tmp = {HashableCounter({'bar': 2, 'shoo': 1}): 5}
tmp = {HashableCounter({'bar': 2, 'shoo': 1}): 15}
True
Related