Recursive DotDict
Question:
I have a utility class that makes Python dictionaries behave somewhat like JavaScript objects as far as getting and setting attributes.
class DotDict(dict):
"""
a dictionary that supports dot notation
as well as dictionary access notation
usage: d = DotDict() or d = DotDict({'val1':'first'})
set attributes: d.val2 = 'second' or d['val2'] = 'second'
get attributes: d.val2 or d['val2']
"""
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
I would like to make it so it also converts nested dictionaries into DotDict() instances. I was hoping to be able to do something like this with __init__
or __new__
, but I haven’t come up with anything that works:
def __init__(self, dct):
for key in dct.keys():
if hasattr(dct[key], 'keys'):
dct[key] = DotDict(dct[key])
How can I recursively convert the nested dictionaries into DotDict() instances?
>>> dct = {'scalar_value':1, 'nested_dict':{'value':2}}
>>> dct = DotDict(dct)
>>> print dct
{'scalar_value': 1, 'nested_dict': {'value': 2}}
>>> print type(dct)
<class '__main__.DotDict'>
>>> print type(dct['nested_dict'])
<type 'dict'>
Answers:
I don’t see where you are copying the values in the constructor. Here DotDict is always empty because of that. When I added the key assignment, it worked:
class DotDict(dict):
"""
a dictionary that supports dot notation
as well as dictionary access notation
usage: d = DotDict() or d = DotDict({'val1':'first'})
set attributes: d.val2 = 'second' or d['val2'] = 'second'
get attributes: d.val2 or d['val2']
"""
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
def __init__(self, dct):
for key, value in dct.items():
if hasattr(value, 'keys'):
value = DotDict(value)
self[key] = value
dct = {'scalar_value':1, 'nested_dict':{'value':2, 'nested_nested': {'x': 21}}}
dct = DotDict(dct)
print dct.nested_dict.nested_nested.x
It looks a bit dangerous and error prone, not to mention source of countless surprises to other developers, but seems to be working.
The accepted answer has some gotchas, such as failing on hasattr()
. Using the keys to simulate properties means you need to do a tad more than assign __getattr__ = dict.__getitem__
. Here’s a more robust implementation with tests:
from collections import OrderedDict, Mapping
class DotDict(OrderedDict):
'''
Quick and dirty implementation of a dot-able dict, which allows access and
assignment via object properties rather than dict indexing.
'''
def __init__(self, *args, **kwargs):
# we could just call super(DotDict, self).__init__(*args, **kwargs)
# but that won't get us nested dotdict objects
od = OrderedDict(*args, **kwargs)
for key, val in od.items():
if isinstance(val, Mapping):
value = DotDict(val)
else:
value = val
self[key] = value
def __delattr__(self, name):
try:
del self[name]
except KeyError as ex:
raise AttributeError(f"No attribute called: {name}") from ex
def __getattr__(self, k):
try:
return self[k]
except KeyError as ex:
raise AttributeError(f"No attribute called: {k}") from ex
__setattr__ = OrderedDict.__setitem__
And the tests:
class DotDictTest(unittest.TestCase):
def test_add(self):
exp = DotDict()
# test that it's not there
self.assertFalse(hasattr(exp, 'abc'))
with self.assertRaises(AttributeError):
_ = exp.abc
with self.assertRaises(KeyError):
_ = exp['abc']
# assign and test that it is there
exp.abc = 123
self.assertTrue(hasattr(exp, 'abc'))
self.assertTrue('abc' in exp)
self.assertEqual(exp.abc, 123)
def test_delete_attribute(self):
exp = DotDict()
# not there
self.assertFalse(hasattr(exp, 'abc'))
with self.assertRaises(AttributeError):
_ = exp.abc
# set value
exp.abc = 123
self.assertTrue(hasattr(exp, 'abc'))
self.assertTrue('abc' in exp)
self.assertEqual(exp.abc, 123)
# delete attribute
delattr(exp, 'abc')
# not there
self.assertFalse(hasattr(exp, 'abc'))
with self.assertRaises(AttributeError):
delattr(exp, 'abc')
def test_delete_key(self):
exp = DotDict()
# not there
self.assertFalse('abc' in exp)
with self.assertRaises(KeyError):
_ = exp['abc']
# set value
exp['abc'] = 123
self.assertTrue(hasattr(exp, 'abc'))
self.assertTrue('abc' in exp)
self.assertEqual(exp.abc, 123)
# delete key
del exp['abc']
# not there
with self.assertRaises(KeyError):
del exp['abc']
def test_change_value(self):
exp = DotDict()
exp.abc = 123
self.assertEqual(exp.abc, 123)
self.assertEqual(exp.abc, exp['abc'])
# change attribute
exp.abc = 456
self.assertEqual(exp.abc, 456)
self.assertEqual(exp.abc, exp['abc'])
# change key
exp['abc'] = 789
self.assertEqual(exp.abc, 789)
self.assertEqual(exp.abc, exp['abc'])
def test_DotDict_dict_init(self):
exp = DotDict({'abc': 123, 'xyz': 456})
self.assertEqual(exp.abc, 123)
self.assertEqual(exp.xyz, 456)
def test_DotDict_named_arg_init(self):
exp = DotDict(abc=123, xyz=456)
self.assertEqual(exp.abc, 123)
self.assertEqual(exp.xyz, 456)
def test_DotDict_datatypes(self):
exp = DotDict({'intval': 1, 'listval': [1, 2, 3], 'dictval': {'a': 1}})
self.assertEqual(exp.intval, 1)
self.assertEqual(exp.listval, [1, 2, 3])
self.assertEqual(exp.listval[0], 1)
self.assertEqual(exp.dictval, {'a': 1})
self.assertEqual(exp.dictval['a'], 1)
self.assertEqual(exp.dictval.a, 1) # nested dotdict works
And just for fun, you can turn an object into a DotDict with this:
def to_dotdict(obj):
''' Converts an object to a DotDict '''
if isinstance(obj, DotDict):
return obj
elif isinstance(obj, Mapping):
return DotDict(obj)
else:
result = DotDict()
for name in dir(obj):
value = getattr(obj, name)
if not name.startswith('__') and not inspect.ismethod(value):
result[name] = value
return result
I’ve been slightly unhappy with all the different answers I have found to this problem. My goals in my implementation were:
1) Don’t create more new object attributes than necessary.
2) Don’t allow overwriting access to built-in attributes.
3) The class converts added items to maintain consistency.
class attrdict(dict):
"""
Attribute Dictionary.
Enables getting/setting/deleting dictionary keys via attributes.
Getting/deleting a non-existent key via attribute raises `AttributeError`.
Objects are passed to `__convert` before `dict.__setitem__` is called.
This class rebinds `__setattr__` to call `dict.__setitem__`. Attributes
will not be set on the object, but will be added as keys to the dictionary.
This prevents overwriting access to built-in attributes. Since we defined
`__getattr__` but left `__getattribute__` alone, built-in attributes will
be returned before `__getattr__` is called. Be careful::
>>> a = attrdict()
>>> a['key'] = 'value'
>>> a.key
'value'
>>> a['keys'] = 'oops'
>>> a.keys
<built-in method keys of attrdict object at 0xabcdef123456>
Use `'key' in a`, not `hasattr(a, 'key')`, as a consequence of the above.
"""
def __init__(self, *args, **kwargs):
# We trust the dict to init itself better than we can.
dict.__init__(self, *args, **kwargs)
# Because of that, we do duplicate work, but it's worth it.
for k, v in self.iteritems():
self.__setitem__(k, v)
def __getattr__(self, k):
try:
return dict.__getitem__(self, k)
except KeyError:
# Maintain consistent syntactical behaviour.
raise AttributeError(
"'attrdict' object has no attribute '" + str(k) + "'"
)
def __setitem__(self, k, v):
dict.__setitem__(self, k, attrdict.__convert(v))
__setattr__ = __setitem__
def __delattr__(self, k):
try:
dict.__delitem__(self, k)
except KeyError:
raise AttributeError(
"'attrdict' object has no attribute '" + str(k) + "'"
)
@staticmethod
def __convert(o):
"""
Recursively convert `dict` objects in `dict`, `list`, `set`, and
`tuple` objects to `attrdict` objects.
"""
if isinstance(o, dict):
o = attrdict(o)
elif isinstance(o, list):
o = list(attrdict.__convert(v) for v in o)
elif isinstance(o, set):
o = set(attrdict.__convert(v) for v in o)
elif isinstance(o, tuple):
o = tuple(attrdict.__convert(v) for v in o)
return o
Shamelessly plugging my own package
There is a package doing exactly what you want and also something more and it is called Prodict.
from prodict import Prodict
life_dict = {'bigBang':
{'stars':
{'planets': []}
}
}
life = Prodict.from_dict(life_dict)
print(life.bigBang.stars.planets)
# prints []
# you can even add new properties dynamically
life.bigBang.galaxies = []
PS 1: I’m the author of the Prodict.
PS 2: This is a direct copy paste of an answer of another question.
I have a utility class that makes Python dictionaries behave somewhat like JavaScript objects as far as getting and setting attributes.
class DotDict(dict):
"""
a dictionary that supports dot notation
as well as dictionary access notation
usage: d = DotDict() or d = DotDict({'val1':'first'})
set attributes: d.val2 = 'second' or d['val2'] = 'second'
get attributes: d.val2 or d['val2']
"""
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
I would like to make it so it also converts nested dictionaries into DotDict() instances. I was hoping to be able to do something like this with __init__
or __new__
, but I haven’t come up with anything that works:
def __init__(self, dct):
for key in dct.keys():
if hasattr(dct[key], 'keys'):
dct[key] = DotDict(dct[key])
How can I recursively convert the nested dictionaries into DotDict() instances?
>>> dct = {'scalar_value':1, 'nested_dict':{'value':2}}
>>> dct = DotDict(dct)
>>> print dct
{'scalar_value': 1, 'nested_dict': {'value': 2}}
>>> print type(dct)
<class '__main__.DotDict'>
>>> print type(dct['nested_dict'])
<type 'dict'>
I don’t see where you are copying the values in the constructor. Here DotDict is always empty because of that. When I added the key assignment, it worked:
class DotDict(dict):
"""
a dictionary that supports dot notation
as well as dictionary access notation
usage: d = DotDict() or d = DotDict({'val1':'first'})
set attributes: d.val2 = 'second' or d['val2'] = 'second'
get attributes: d.val2 or d['val2']
"""
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
def __init__(self, dct):
for key, value in dct.items():
if hasattr(value, 'keys'):
value = DotDict(value)
self[key] = value
dct = {'scalar_value':1, 'nested_dict':{'value':2, 'nested_nested': {'x': 21}}}
dct = DotDict(dct)
print dct.nested_dict.nested_nested.x
It looks a bit dangerous and error prone, not to mention source of countless surprises to other developers, but seems to be working.
The accepted answer has some gotchas, such as failing on hasattr()
. Using the keys to simulate properties means you need to do a tad more than assign __getattr__ = dict.__getitem__
. Here’s a more robust implementation with tests:
from collections import OrderedDict, Mapping
class DotDict(OrderedDict):
'''
Quick and dirty implementation of a dot-able dict, which allows access and
assignment via object properties rather than dict indexing.
'''
def __init__(self, *args, **kwargs):
# we could just call super(DotDict, self).__init__(*args, **kwargs)
# but that won't get us nested dotdict objects
od = OrderedDict(*args, **kwargs)
for key, val in od.items():
if isinstance(val, Mapping):
value = DotDict(val)
else:
value = val
self[key] = value
def __delattr__(self, name):
try:
del self[name]
except KeyError as ex:
raise AttributeError(f"No attribute called: {name}") from ex
def __getattr__(self, k):
try:
return self[k]
except KeyError as ex:
raise AttributeError(f"No attribute called: {k}") from ex
__setattr__ = OrderedDict.__setitem__
And the tests:
class DotDictTest(unittest.TestCase):
def test_add(self):
exp = DotDict()
# test that it's not there
self.assertFalse(hasattr(exp, 'abc'))
with self.assertRaises(AttributeError):
_ = exp.abc
with self.assertRaises(KeyError):
_ = exp['abc']
# assign and test that it is there
exp.abc = 123
self.assertTrue(hasattr(exp, 'abc'))
self.assertTrue('abc' in exp)
self.assertEqual(exp.abc, 123)
def test_delete_attribute(self):
exp = DotDict()
# not there
self.assertFalse(hasattr(exp, 'abc'))
with self.assertRaises(AttributeError):
_ = exp.abc
# set value
exp.abc = 123
self.assertTrue(hasattr(exp, 'abc'))
self.assertTrue('abc' in exp)
self.assertEqual(exp.abc, 123)
# delete attribute
delattr(exp, 'abc')
# not there
self.assertFalse(hasattr(exp, 'abc'))
with self.assertRaises(AttributeError):
delattr(exp, 'abc')
def test_delete_key(self):
exp = DotDict()
# not there
self.assertFalse('abc' in exp)
with self.assertRaises(KeyError):
_ = exp['abc']
# set value
exp['abc'] = 123
self.assertTrue(hasattr(exp, 'abc'))
self.assertTrue('abc' in exp)
self.assertEqual(exp.abc, 123)
# delete key
del exp['abc']
# not there
with self.assertRaises(KeyError):
del exp['abc']
def test_change_value(self):
exp = DotDict()
exp.abc = 123
self.assertEqual(exp.abc, 123)
self.assertEqual(exp.abc, exp['abc'])
# change attribute
exp.abc = 456
self.assertEqual(exp.abc, 456)
self.assertEqual(exp.abc, exp['abc'])
# change key
exp['abc'] = 789
self.assertEqual(exp.abc, 789)
self.assertEqual(exp.abc, exp['abc'])
def test_DotDict_dict_init(self):
exp = DotDict({'abc': 123, 'xyz': 456})
self.assertEqual(exp.abc, 123)
self.assertEqual(exp.xyz, 456)
def test_DotDict_named_arg_init(self):
exp = DotDict(abc=123, xyz=456)
self.assertEqual(exp.abc, 123)
self.assertEqual(exp.xyz, 456)
def test_DotDict_datatypes(self):
exp = DotDict({'intval': 1, 'listval': [1, 2, 3], 'dictval': {'a': 1}})
self.assertEqual(exp.intval, 1)
self.assertEqual(exp.listval, [1, 2, 3])
self.assertEqual(exp.listval[0], 1)
self.assertEqual(exp.dictval, {'a': 1})
self.assertEqual(exp.dictval['a'], 1)
self.assertEqual(exp.dictval.a, 1) # nested dotdict works
And just for fun, you can turn an object into a DotDict with this:
def to_dotdict(obj):
''' Converts an object to a DotDict '''
if isinstance(obj, DotDict):
return obj
elif isinstance(obj, Mapping):
return DotDict(obj)
else:
result = DotDict()
for name in dir(obj):
value = getattr(obj, name)
if not name.startswith('__') and not inspect.ismethod(value):
result[name] = value
return result
I’ve been slightly unhappy with all the different answers I have found to this problem. My goals in my implementation were:
1) Don’t create more new object attributes than necessary.
2) Don’t allow overwriting access to built-in attributes.
3) The class converts added items to maintain consistency.
class attrdict(dict):
"""
Attribute Dictionary.
Enables getting/setting/deleting dictionary keys via attributes.
Getting/deleting a non-existent key via attribute raises `AttributeError`.
Objects are passed to `__convert` before `dict.__setitem__` is called.
This class rebinds `__setattr__` to call `dict.__setitem__`. Attributes
will not be set on the object, but will be added as keys to the dictionary.
This prevents overwriting access to built-in attributes. Since we defined
`__getattr__` but left `__getattribute__` alone, built-in attributes will
be returned before `__getattr__` is called. Be careful::
>>> a = attrdict()
>>> a['key'] = 'value'
>>> a.key
'value'
>>> a['keys'] = 'oops'
>>> a.keys
<built-in method keys of attrdict object at 0xabcdef123456>
Use `'key' in a`, not `hasattr(a, 'key')`, as a consequence of the above.
"""
def __init__(self, *args, **kwargs):
# We trust the dict to init itself better than we can.
dict.__init__(self, *args, **kwargs)
# Because of that, we do duplicate work, but it's worth it.
for k, v in self.iteritems():
self.__setitem__(k, v)
def __getattr__(self, k):
try:
return dict.__getitem__(self, k)
except KeyError:
# Maintain consistent syntactical behaviour.
raise AttributeError(
"'attrdict' object has no attribute '" + str(k) + "'"
)
def __setitem__(self, k, v):
dict.__setitem__(self, k, attrdict.__convert(v))
__setattr__ = __setitem__
def __delattr__(self, k):
try:
dict.__delitem__(self, k)
except KeyError:
raise AttributeError(
"'attrdict' object has no attribute '" + str(k) + "'"
)
@staticmethod
def __convert(o):
"""
Recursively convert `dict` objects in `dict`, `list`, `set`, and
`tuple` objects to `attrdict` objects.
"""
if isinstance(o, dict):
o = attrdict(o)
elif isinstance(o, list):
o = list(attrdict.__convert(v) for v in o)
elif isinstance(o, set):
o = set(attrdict.__convert(v) for v in o)
elif isinstance(o, tuple):
o = tuple(attrdict.__convert(v) for v in o)
return o
Shamelessly plugging my own package
There is a package doing exactly what you want and also something more and it is called Prodict.
from prodict import Prodict
life_dict = {'bigBang':
{'stars':
{'planets': []}
}
}
life = Prodict.from_dict(life_dict)
print(life.bigBang.stars.planets)
# prints []
# you can even add new properties dynamically
life.bigBang.galaxies = []
PS 1: I’m the author of the Prodict.
PS 2: This is a direct copy paste of an answer of another question.