3

Having a dict like this

my_pets = {
    'Rudolf': {
        'animal': 'cat', 
        'legs': 4
    }
}

What is the cleaner way of achieving below equivalent?

my_pets['Rudolf']['legs']['front-right']['injured'] = True
my_pets['Rudolf']['legs']['front-left']['injured'] = False

And it should update as

my_pets = {
    'Rudolf': {
        'animal': 'cat', 
        'legs': {
            'front-right': {'injured':True},
            'front-left': {'injured':False}
        }
    }
}
nehem
  • 12,775
  • 6
  • 58
  • 84
  • I think you want to make a few classes here, instead of putting all your data in nested dicts. That's the only way I can think of to make it "cleaner". Note: with your current data representation, your way is the cleanest possible way to access – inspectorG4dget Nov 12 '15 at 02:20
  • Edited the question slightly, The ugliness I am into now is, I had to manually chain them by checking for their existence and creating empty dicts on non-existence and move to next depth. – nehem Nov 12 '15 at 02:25
  • Use a `collections.defaultdict(lambda: collections.defaultdict)` instead of a dict. That might help with the updating process – inspectorG4dget Nov 12 '15 at 02:26
  • @inspectorG4dget That's the right direction, but won't work past one level, since accessing a missing key at the top level creates a `defaultdict` without an initialized default factory. So whenever you try and access a missing key at the *second* level, a `KeyError` is raised. – jme Nov 12 '15 at 03:05

5 Answers5

6

You could create an "infinite" defaultdict, as follows:

from collections import defaultdict

def infinidict():
    return defaultdict(infinidict)

Then writing:

>>> my_pets = infinidict()
>>> my_pets['Rudolf']['animal'] = 'cat'
>>> my_pets['Rudolf']['weight'] = 3
>>> my_pets['Rudolf']['legs']['front-right']['injured'] = True
>>> my_pets
defaultdict(<function __main__.infinidict>,
            {'Rudolf': defaultdict(<function __main__.infinidict>,
                         {'animal': 'cat',
                          'legs': defaultdict(<function __main__.infinidict>,
                                      {'front-right': defaultdict(<function __main__.infinidict>,
                                                   {'injured': True})}),
                          'weight': 3})})

The output looks messy, but my_pets can be used wherever a dict is required.

jme
  • 19,895
  • 6
  • 41
  • 39
  • Neat trick. I always considered the "limited depth" of `defaultdict` to be a big drawback, did not think of doing something like this. – Igor Raush Nov 12 '15 at 03:06
5

Below is a dictionary subclass which is lenient to missing keys up to an arbitrary depth:

class freedict(dict):
    # called when trying to read a missing key
    def __missing__(self, key):
        self[key] = freedict()
        return self[key]

    # called during attribute access
    # note that this invokes __missing__ above
    def __getattr__(self, key):
        return self[key]

    # called during attribute assignment
    def __setattr__(self, key, value):
        self[key] = value

This can be used like so (attribute access to keys is a personal preference):

d = freedict()
d['one']['two']['three'] = 1
d.one.two.three = 2
Igor Raush
  • 15,080
  • 1
  • 34
  • 55
  • 1
    Pretty clean, and I think preferable to recursive defaultdicts, since printing an instance looks just like what you'd get with a `dict`. – jme Nov 12 '15 at 03:11
  • Sounds neat, but might break in few cases, For ex: `d = freedict()⏎ d['one'] = 1⏎ d['one']['two']['three'] = 1` – nehem Nov 12 '15 at 05:23
  • @itsneo true, but what else would you expect to happen in this scenario? I'd argue that it should break. – Igor Raush Nov 12 '15 at 05:28
  • I suppose you could code up a tree data structure which can hold values at internal nodes. But at that point, it's not really a `dict` anymore, and it would probably need to have a special way of accessing values. I don't think an API like what you wrote in the comment would actually be possible, although I will think more about it. – Igor Raush Nov 12 '15 at 05:33
  • 1
    @igor, I do get you, Yet in the breaking case above we can expect `d['one']['two']['three'] = 1` to produce `{'one': {'two': {'three': 1}}}` which is what your original code produces. However if `d['one']` is already presented it's unable to replace them. Remember how a normal dict assignment works, It simply replaces existing value, which doesn't happen here. – nehem Nov 12 '15 at 09:31
3

This is a very interesting and a very practical situation one can encounter. There are numerous implementations, each of which solve certain problems but miss out on a few edge cases.

Possible solutions and varying answers can be found in these titles.

What is the best way to implement nested dictionaries?

What's the best way to initialize a dict of dicts in Python?

Set nested dict value and create intermediate keys

Also, there are numerous gists and blogs found on this requirement 'autovivification', including a wikipedia presence.

http://blog.yjl.im/2013/08/autovivification-in-python.html

https://news.ycombinator.com/item?id=3881171

https://gist.github.com/hrldcpr/2012250

https://en.wikipedia.org/wiki/Autovivification

http://blogs.fluidinfo.com/terry/2012/05/26/autovivification-in-python-nested-defaultdicts-with-a-specific-final-type/

While the above implementations are handy once, edge cases can still be problematic. At the time of this writing, no implementation has handled well whether there is a primitive sitting and blocking the nest.

Here are the 3 main ways this question and related questions are answered here in StackOverflow.

  • Write a helper method, that accepts a dictionary, value and list of nested keys. Works well with plain dict objects, but lacks the usual square bracket syntax.

  • Use Defaultdict and write a custom class. Fundamentally this works since default dict supplies {} for missing keys. Great syntax, but works only for the objects that were created using the custom class.

  • Use tuples to store and retrieve (https://stackoverflow.com/a/651930/968442). The Worst idea of all. Should not even be claimed as a solution. Here is why

    mydict = {}
    mydict['foo', 'bar', 'baz'] = 1
    print mydict['foo', 'bar', 'baz']

    Will work fine, but when you access mydict['foo', 'bar'] the expectation will be {'baz':1}, not a KeyError. This basically destroys the idea of iterable & nested structure.

Of the three approaches, my bet goes to option 1. By writing a tiny helper method the edge cases can be resolved pragmatically, here is my implementation.

def sattr(d, *attrs):
    # Adds "val" to dict in the hierarchy mentioned via *attrs
    for attr in attrs[:-2]:
        # If such key is not found or the value is primitive supply an empty dict
        if d.get(attr) is None or not isinstance(d.get(attr), dict):
            d[attr] = {}
        d = d[attr]
    d[attrs[-2]] = attrs[-1]

Now

my_pets = {'Rudolf': {'animal': 'cat', 'legs': 4}}
sattr(my_pets, 'Rudolf', 'legs', 'front-right', 'injured', True)
sattr(my_pets, 'Rudolf', 'legs', 'front-left', 'injured', False)

will produce

{'Rudolf': {'animal': 'cat', 'legs': 4}}
{'Rudolf': {'animal': 'cat', 'legs': {'front-right': {'injured': True}}}}
{'Rudolf': {'animal': 'cat', 'legs': {'front-right': {'injured': True},  'front-left': {'injured': False}}}}
nehem
  • 12,775
  • 6
  • 58
  • 84
0

Try using try

try:

    # checks whether 'front-right' exists. If exists assigns value. Else raises   exception
    my_pets['Rudolf']['legs']['front-right']= {'injured':True}}

except:

    # On raising exception add 'front-right' to 'legs'
    my_pets['Rudolf']['legs'] = {'front-right': {'injured':True}}

this should work

Harwee
  • 1,601
  • 2
  • 21
  • 35
0

This will allow you to add a key at any any depth to a dict based on list of the keys.

  def add_multi_key(subscripts, _dict={}, val=None):
        """Add an arbitrary length key to a dict.

        Example:

            out = add_multi_key(['a','b','c'], {}, 1 )

            out -> {'a': {'b': {'c':1}}}

        Arguments:
            subscripts, list of keys to add
            _dict, dict to update. Default is {}
            val, any legal value to a key. Default is None.

        Returns:
            _dict - dict with added key.
        """
        if not subscripts:
            return _dict
        subscripts = [s.strip() for s in subscripts]
        for sub in subscripts[:-1]:
            if '_x' not in locals():
                if sub not in _dict:
                    _dict[sub] = {}
                _x = _dict.get(sub)
            else:
                if sub not in _x:
                    _x[sub] = {}
                _x = _x.get(sub)
        _x[subscripts[-1]] = val
        return _dict
joel3000
  • 1,249
  • 11
  • 22