I wrote a class that extends the dict class to provide indexing like functionality, so I can find which key(s) map to a given value in the dict. It's pretty bare bones and there are a lot of things I haven't fleshed out like docstrings (will add these later when I have a final version), how to handle unhashable elements (it currently just fails, but this might be changed in the future), etc.

The extremely bare bones nature of this is because this is something I whipped up in a few minutes for immediate use.

from collections import defaultdict  # The class currently doesn't exist in a separate module, and there are many other imports used by surrounding code.

# Do something

class DoubleDict(dict):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.values_dict = defaultdict(lambda: [])
        for key, value in self.items():
            self.values_dict[value] += [key]

    def __setitem__(self, key, value):
        var = self.get(key)
        if var in self.values_dict:
            if not self.values_dict[var]:
                del self.values_dict[var]
        super().__setitem__(key, value)
        self.values_dict[value] += [key]

    def index(self, value):
        return self.values_dict[value][-1]

    def indices(self, value):
        return self.values_dict[value]

# Do Something

My concerns are to have this be as performant as feasible, and sane and generally correct (there may be some unavoidable tradeoffs) approach. I would probably make this into its own module after incorporating feedback.

  • \$\begingroup\$ Why/how would you use this? Is this an alternative to turning them inside-out? \$\endgroup\$ – Mast Dec 14 '19 at 19:26
  • \$\begingroup\$ For finding the keys that map to a given value. \$\endgroup\$ – Tobi Alafin Dec 14 '19 at 19:30
  • 1
    \$\begingroup\$ Your latest edit invalidated part of my answer. And so I have rolled it back as per site rules. Lets not post incomplete questions, and then edit it whilst answerers are trying to answer you question. That's just bad form. \$\endgroup\$ – Peilonrayz Dec 14 '19 at 19:52

A couple of minor notes:

1) Add proper typing so that users of your code will be able to type-check their own code correctly. I think this would look like:

from typing import Dict, List, TypeVar

_KT = TypeVar('_KT')
_VT = TypeVar('_VT')

class DoubleDict(Dict[_KT, _VT]):
    super().__init__(*args, **kwargs)
    self.values_dict: Dict[_VT, List[_KT]] = defaultdict(lambda: [])
    for key, value in self.items():
        self.values_dict[value] += [key]

... etc

2) I'd eliminate the index syntactic sugar, personally, because it makes it easier for the caller to forget that this isn't a one-to-one mapping in both directions. Make them get the list and check to make sure that it's got the length they expect.

Alternatively, if you'd want it to be a one-to-one mapping in most cases, then enforce that in the setter and make it part of the class's contract.

| improve this answer | |
  • \$\begingroup\$ what does typing do? \$\endgroup\$ – Noone AtAll Dec 15 '19 at 7:55
  • \$\begingroup\$ It makes you not hate your life when you're maintaining your code. \$\endgroup\$ – Samwise Dec 15 '19 at 16:58
  • \$\begingroup\$ see: mypy-lang.org If you're writing Python code without mypy you're going to have a bad time. \$\endgroup\$ – Samwise Dec 15 '19 at 17:04

I don't see the point in this, further more if you want to hash unhashable things then you probably have a design problem.

Realistically this class lulls you into a false sense of security.

>>> dd = DoubleDict()
>>> dd[1] = 2
>>> dd.update({'foo': 'bar'})
>>> del dd[1]
>>> dict(dd.values_dict)
{2: [1]}
>>> dd
{'foo': 'bar'}

If you haven't implemented all edge cases don't inherit. Instead just make the class an interface. If "there are a lot of things I haven't fleshed out" then don't inherit. Some may argue just don't inherit period.

I think inheritance is good if you know how to use it. Like if you inherit collections.abc.MutableMapping. By defining a meager 5 methods you too can have a complete dictionary. Better yet, if you inherit from typing.MutableMapping you can have the object be fully typed too - if you type the abstract methods.

| improve this answer | |
  • \$\begingroup\$ I've edited my answer to reflect my current version of the class. (It properly updates the .values_dict upon item reassignment and deletion). (I should have probably fully fleshed it out more before making it a question). I would probably make a new question if I make significant further improvements. \$\endgroup\$ – Tobi Alafin Dec 14 '19 at 19:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.