1

Let's say I have a nested dictionary with depth N. How can I convert each inner nested dictionary into a simple namespace?

example_input = {key0a: "test", key0b: {key1a: {key2a: {...keyNx}, key2b: "test"} ,key1b: "test"}}

example_output = SimpleNamespace(key0a: "test", key0b: SimpleNamespace(key1a: SimpleNamespace(key2a: SimpleNamespace(...keyNx), key2b: "test"), key1b: "test"))

Are there better alternatives to make the keys of the dictionary accessible per dot notation (e.g. example_input.key0a) if the example_input dict is given - without having external dependencies?

BugsBuggy
  • 53
  • 6
  • There are different non-standard projects for doing this like [attrdict](https://pypi.org/project/attrdict/). – Arty Feb 15 '21 at 12:39

3 Answers3

2

Just answering your second (last) question - it is popular topic, there exist many different projects (non-standard) that make your dictionary into dot notation object.

For example this one - attrdict. Install it through pip install attrdict.

Example of usage:

Try it online!

from attrdict import AttrDict
d = {'a': 1, 'b': [{'c': 2}, {'d': {'e': {'f': {5: {'g': 3}}}}}]}
ad = AttrDict(d)
print(ad.b[1].d.e.f(5).g) # 3

If you wonder how module like attrdict is implemented, then I wrote a very simple implementation of similar functionality (of course real attrdict should be more rich):

Try it online!

class AttrD(object):
    def __init__(self, d = {}):
        self.set_d(d)
    def __getattr__(self, key):
        return AttrD(self.get_or_create(key))
    def __setattr__(self, key, value):
        self.set_or_create(key, value)
    def __getitem__(self, key):
        return AttrD(self.get_or_create(key))
    def __setitem__(self, key, value):
        self.set_or_create(key, value)
    def __call__(self, key):
        return AttrD(self.get_or_create(key))
    def __repr__(self):
        return repr(self._d)
    def to_obj(self):
        return self._d
    def set_d(self, d):
        super(AttrD, self).__setattr__('_d', d)
    def get_or_create(self, name):
        if type(self._d) in (dict,) and name not in self._d:
            self._d[name] = {}
        if type(self._d) in (list, tuple) and len(self._d) <= name:
            self.set_d(self._d + type(self._d)(
                [None] * (name + 1 - len(self._d))))
        return self._d[name]
    def set_or_create(self, key, value):
        self.get_or_create(key)
        self._d[key] = value

ad = AttrD({'a': 1, 'b': [{'c': 2}, {'d': {'e': {'f': {5: {'g': 3}}}}}]})
ad.b[1].d.e.f(5).g = [4, 5, 6]
print(ad.b[1].d.e.f(5).g[2]) # 6
print(AttrD({'a': 123}).b.c) # Non-existent defaults to {}
Arty
  • 12,357
  • 4
  • 25
  • 41
1

Using recursion

example_input = {'key0a': "test", 'key0b': 
                 {'key1a': {'key2a': 'end', 'key2b': "test"} ,'key1b': "test"}, 
                 "something": "else"}
def parse(d):
  x = SimpleNamespace()
  _ = [setattr(x, k, parse(v)) if isinstance(v, dict) else setattr(x, k, v) for k, v in d.items() ]    
  return x

result = parse(example_input)
print (result)

Output:

namespace(key0a='test', 
          key0b=namespace(key1a=namespace(key2a='end', key2b='test'), key1b='test'), 
          something='else')
mujjiga
  • 14,755
  • 2
  • 28
  • 43
0

Based on mujjija's solution this is what I came up with. Full code below

from types import SimpleNamespace


def parse(data):
    if type(data) is list:
        return list(map(parse, data))
    elif type(data) is dict:
        sns = SimpleNamespace()
        for key, value in data.items():
            setattr(sns, key, parse(value))
        return sns
    else:
        return data


info = {
    'country': 'Australia',
    'number': 1,
    'slangs': [
        'no worries mate',
        'winner winner chicken dinner',
        {
            'no_slangs': [123, {'definately_not': 'hello'}]
        }
    ],
    'tradie': {
        'name': 'Rizza',
        'occupation': 'sparkie'
    }
}

d = parse(info)
assert d.country == 'Australia'
assert d.number == 1
assert d.slangs[0] == 'no worries mate'
assert d.slangs[1] == 'winner winner chicken dinner'
assert d.slangs[2].no_slangs[0] == 123
assert d.slangs[2].no_slangs[1].definately_not == 'hello'
assert d.tradie.name == 'Rizza'
assert d.tradie.occupation == 'sparkie'

If I'm not mistaken, Python doesn't support Tail Call Optimization. So please be careful when using deep recursive functions in Python. For small examples, it should be fine.

Update

Another version. object_hook does the magic of nesting. I prefer this version because I can directly feed them to the jinja2 template engine.

import json


class _DotDict(dict):
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__


def dot(data=None):
    if data is []:
        return []
    return json.loads(json.dumps(data), object_hook=_DotDict) if data else _DotDict()
Eddie
  • 1,003
  • 8
  • 14