Reputation: 159
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?
Upvotes: 5
Views: 5939
Reputation: 359
See RecursiveNamespace package at https://pypi.org/project/RecursiveNamespace/. You can convert a dictionary to a recursive namespace, and back.
From the README:
from recursivenamespace import rns # or RecursiveNamespace
data = {
'name': 'John',
'age': 30,
'address': {
'street': '123 Main St',
'city': 'Anytown'
},
'friends': ['Jane', 'Tom']
}
rn = rns(data)
print(type(rn)) # <class 'recursivenamespace.main.recursivenamespace'>
print(rn) # RNS(name=John, age=30, address=RNS(street=123 Main St, city=Anytown))
print(rn.name) # John
print(rn.address.city) # Anytown
print(rn.friends[1]) # Tom, yes it does recognize iterables
# convert back to dictionary
data2 = rn.to_dict()
print(type(data2)) # <class 'dict'>
print(data2 == data) # True
print(data2['address']['city']) # Anytown
print(data2['friends'][1]) # Tom
You can also YAML:
import yaml
from recursivenamespace import rns
datatext = """
name: John
age: 30
address:
street: 123 Main St
city: Anytown
friends:
- Jane
- Tom
"""
data = yaml.safe_load(datatext)
rn = rns(data)
print(rn) # RNS(name=John, age=30, address=RNS(street=123 Main St, city=Anytown))
# convert back to YAML
data_yaml = yaml.dump(rn.to_dict())
To install, run python -m pip install RecursiveNamespace
.
Upvotes: 0
Reputation: 11550
I know its late but stumbled upon similar problem with not easy solution. I crated this class which can be used same as SimpleNamespace but with recustion built in. Thanks.
from typing import Any
from types import SimpleNamespace as SNS
class RecursiveNS(SNS):
def __init__(self, **kwargs):
self.__dict__.update(self.parse(kwargs).__dict__)
@staticmethod
def parse(d: dict[str, Any]) -> SNS:
"""Static method that takes dictionary as an argument,
and returns """
x = SNS()
for k, v in d.items():
setattr(
x,
k,
RecursiveNS.parse(v)
if isinstance(v, dict)
else [RecursiveNS.parse(e) for e in v]
if isinstance(v, list)
else v,
)
return x
Upvotes: 0
Reputation: 11621
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 nesteddict
s within alist
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
Upvotes: 5
Reputation: 16747
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:
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):
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 {}
Upvotes: 2
Reputation: 1063
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()
Upvotes: 2
Reputation: 16896
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')
Upvotes: 1