nehem
nehem

Reputation: 13642

Python adding/updating dict element of any depth

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}
        }
    }
}

Upvotes: 3

Views: 3849

Answers (5)

nehem
nehem

Reputation: 13642

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}}}}

Upvotes: 3

joel3000
joel3000

Reputation: 1319

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

Upvotes: 0

Igor Raush
Igor Raush

Reputation: 15240

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

Upvotes: 5

jme
jme

Reputation: 20695

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.

Upvotes: 6

Harwee
Harwee

Reputation: 1650

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

Upvotes: 0

Related Questions