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
  • 9,162
  • 6
  • 44
  • 71
  • 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

5

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
  • 16,819
  • 5
  • 33
  • 38
  • 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
  • 14,155
  • 1
  • 30
  • 51
  • 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
2

This is a very interesting and a very practical situation once can encounter. There are numerous implementation each solves certain things and miss out few edge scenarios.

Possible solutions and varying answer 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 numerous gists and blogs are 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 implementation are handy once edge case could be still 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 mains ways this question and related questions are answered here in StackOverflow.

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

  • Use Defaultdict and write 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, Not even should be claimed as 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 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': {'legs': 4, 'animal': 'cat'}}
{'Rudolf': {'legs': {'front-right': {'injured': True}}, 'animal': 'cat'}}
{'Rudolf': {'legs': {'front-left': {'injured': False}, 'front-right': {'injured': True}}, 'animal': 'cat'}}
nehem
  • 9,162
  • 6
  • 44
  • 71
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,411
  • 1
  • 16
  • 32
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,039
  • 9
  • 17