简体   繁体   中英

How to convert a nested python dictionary into a simple namespace?

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 (eg example_input.key0a) if the example_input dict is given - without having external dependencies?

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

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')

2022 answer: now there is a tiny, relatively fast library I have published, called dotwiz , which alternatively can be used to provide easy dot access for a python dict object.

It should, coincidentally, be a little faster than the other options -- I've added a quick and dirty benchmark code I put together using the timeit module below, timing against both a attrdict and SimpleNamespace approach -- the latter of which actually performs pretty solid in times.

Note that I had to modify the parse function slightly, so that it handles nested dict s within a list object, for example.

from timeit import timeit
from types import SimpleNamespace

from attrdict import AttrDict
from dotwiz import DotWiz


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 [parse(e) for e in v] if isinstance(v, list)
                 else v) for k, v in d.items()]
    return x


print('-- Create')
print('attrdict:         ', round(timeit('AttrDict(example_input)', globals=globals()), 2))
print('dotwiz:           ', round(timeit('DotWiz(example_input)', globals=globals()), 2))
print('SimpleNamespace:  ', round(timeit('parse(example_input)', globals=globals()), 2))
print()

dw = DotWiz(example_input)
ns = parse(example_input)
ad = AttrDict(example_input)

print('-- Get')
print('attrdict:         ', round(timeit('ad.key0b.key1a[0].key2a', globals=globals()), 2))
print('dotwiz:           ', round(timeit('dw.key0b.key1a[0].key2a', globals=globals()), 2))
print('SimpleNamespace:  ', round(timeit('ns.key0b.key1a[0].key2a', globals=globals()), 2))
print()

print(ad)
print(dw)
print(ns)

assert ad.key0b.key1a[0].key2a \
       == dw.key0b.key1a[0].key2a \
       == ns.key0b.key1a[0].key2a \
       == 'end'

Here are the results, on my M1 Mac Pro laptop:

attrdict:          0.69
dotwiz:            1.3
SimpleNamespace:   1.38

-- Get
attrdict:          6.06
dotwiz:            0.06
SimpleNamespace:   0.06

The dotwiz library can be installed with pip :

$ pip install dotwiz

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()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM