3

I want to make a 2d dictionary with multiple keys per value. I do not want to make a tuple a key. But rather make many keys that will return the same value.

I know how to make a 2d dictionary using defaultdict:

from collections import defaultdict
a_dict = defaultdict(dict)

a_dict['canned_food']['spam'] = 'delicious'

And I can make a tuple a key using

a_dict['food','canned_food']['spam'] = 'delicious'

But this does not allow me to do something like

print a_dict['canned_food']['spam']

Because 'canned_food' is not a key the tuple ['food','canned_food'] is the key.

I have learned that I can simply set many to same value independently like:

a_dict['food']['spam'] = 'delicious'
a_dict['canned_food']['spam'] = 'delicious'

But this becomes messy with a large number of keys. In the first dimension of dictionary I need ~25 keys per value. Is there a way to write the dictionary so that any key in the tuple will work?

I have asked this question before but was not clear on what I wanted so I am reposting. Thank you in advance for any help.

Fnord
  • 5,365
  • 4
  • 31
  • 48
Keith
  • 361
  • 1
  • 5
  • 18
  • 1
    Technically, you are making a tuple a key, not a list. – Gareth Latty Nov 12 '12 at 00:24
  • It's a little unclear how you want this to act. Can a value be in multiple keys? – Gareth Latty Nov 12 '12 at 00:30
  • Yes a single value will have multiple keys. – Keith Nov 12 '12 at 00:31
  • Sorry, I was unclear what I meant there, what I meant was if you have tuples of keys (where they all refer to the same item), can those tuples potentially have the same 'subkeys'? E.g: ``('food', 'canned_food')``, and ``('canned_food', 'canned_beans')`` both being keys. If so, how should they behave? – Gareth Latty Nov 12 '12 at 00:32
  • I think I understand what you are getting at. The keys in the first dimension of the dictionary will always be different then the keys in the second dimension of the dictionary. It is hierarchy. I want to assign a value to something if it is one category and also in a certain subcategory. My problem is that I have many category all with the same subcategory. – Keith Nov 12 '12 at 00:40
  • No, I think you are still misunderstanding. You are asking if you can assign to a sequence of keys and then have them all act as aliases, but what I'm asking is if those aliases are unique, and what happens if you try to access with a list of two values which are already aliases to different items? – Gareth Latty Nov 12 '12 at 00:43
  • Thank you for the clarification. All of the aliases are unique, there will never be a duplicate. – Keith Nov 12 '12 at 00:45
  • https://datascience.stackexchange.com/questions/49529/how-to-create-dictionary-with-multiple-keys-from-dataframe-in-python?noredirect=1#comment56604_49529 Can you please answer this question: – KHAN irfan Apr 18 '19 at 12:53

2 Answers2

9

Here is a possible solution:

from collections import Iterable

class AliasDefaultDict():
    def __init__(self, default_factory, initial=[]):
        self.aliases = {}
        self.data = {}
        self.factory = default_factory
        for aliases, value in initial:
            self[aliases] = value

    @staticmethod
    def distinguish_keys(key):
        if isinstance(key, Iterable) and not isinstance(key, str):
            return set(key)
        else:
            return {key}

    def __getitem__(self, key):
        keys = self.distinguish_keys(key)
        if keys & self.aliases.keys():
            return self.data[self.aliases[keys.pop()]]
        else:
            value = self.factory()
            self[keys] = value
            return value

    def __setitem__(self, key, value):
        keys = self.distinguish_keys(key)
        if keys & self.aliases.keys():
            self.data[self.aliases[keys.pop()]] = value
        else:
            new_key = object()
            self.data[new_key] = value
            for key in keys:
                self.aliases[key] = new_key
            return value

    def __repr__(self):
        representation = defaultdict(list)
        for alias, value in self.aliases.items():
            representation[value].append(alias)
        return "AliasDefaultDict({}, {})".format(repr(self.factory), repr([(aliases, self.data[value]) for value, aliases in representation.items()]))

Which can be used like so:

>>> a_dict = AliasDefaultDict(dict)
>>> a_dict['food', 'canned_food']['spam'] = 'delicious'
>>> a_dict['food']
{'spam': 'delicious'}
>>> a_dict['canned_food']
{'spam': 'delicious'}
>> a_dict
AliasDefaultDict(<class 'dict'>, [(['food', 'canned_food'], {'spam': 'delicious'})])

Note there are some edge cases with undefined behavior - such as using the same key for multiple aliases. I feel this makes this data type pretty awful for general use, and I'd suggest that you may be better off changing your program not to need this kind of overly convoluted structure instead.

Also note this solution is for 3.x, under 2.x, you will want to swap out str for basestring, and self.aliases.keys() for self.aliases.viewkeys().

Gareth Latty
  • 86,389
  • 17
  • 178
  • 183
  • Thank you that seems to work nicely. I did have to change `&` to `and` in line 13 for it to work. I don't really understand though, I will have to look at the code carefully, I am just starting out with python. – Keith Nov 12 '12 at 01:23
  • 2
    ``&`` and ``and`` are different, and not equivalent here. The issue is probably you are using 2.x, while I am on 3.x - in that case, the trick is to have ``self.aliases.viewkeys()`` instead, to make it work. What I am doing there is set intersection, and the list that 2.x returns from ``keys()`` isn't set-like and so it will fail. – Gareth Latty Nov 12 '12 at 01:28
  • Sorry for my ignorance. So I changed `self.aliases.keys():` to `self.aliases.viewkeys():` but i have an error: Traceback (most recent call last): File "/Users/keithfritzsching/Text-3.py", line 30, in a_dict = AliasDefaultDict() TypeError: __init__() takes exactly 2 arguments (1 given) – Keith Nov 12 '12 at 01:39
  • 1
    Like a ``collections.defaultdict``, you need to pass the default value function. In your case, ``dict``. Sorry, I didn't update my example. – Gareth Latty Nov 12 '12 at 01:41
  • Well Thank you very much for your solution. I will have to work a little to understand it but you were extraordinarily helpful! – Keith Nov 12 '12 at 01:47
2

Does this help at all?

class MultiDict(dict):
    # define __setitem__ to set multiple keys if the key is iterable
    def __setitem__(self, key, value):
        try:
            # attempt to iterate though items in the key
            for val in key:
                dict.__setitem__(self, val, value)
        except:
            # not iterable (or some other error, but just a demo)
            # just set that key
            dict.__setitem__(self, key, value)



x = MultiDict()

x["a"]=10
x["b","c"] = 20

print x

The output is

{'a': 10, 'c': 20, 'b': 20}
Lucas
  • 1,869
  • 4
  • 20
  • 36
  • This won't work as the asker isn't assigning to the keys, he is assigning to a dictionary under the keys. Also, why iterate over ``key.__iter__()`` instead of ``key`` directly, and why the casual ``except:`` - catching all exceptions is always a bad idea. – Gareth Latty Nov 12 '12 at 00:46
  • 1
    Yeah, its hacky, but was a suggestion to be implemented better if it was sensible. But yes, point accepted, it's not the right answer. One difficulty if you use a condenced representation is that you might end up with duplicate keys – Lucas Nov 12 '12 at 00:56
  • 1
    Actually, I think it solves the problem when the value is another dictionary – Lucas Nov 12 '12 at 01:03