接受的答案有一些问题,例如在hasattr()
. 使用键来模拟属性意味着您需要做的不仅仅是 assign __getattr__ = dict.__getitem__
。这是一个更强大的测试实现:
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__
和测试:
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
只是为了好玩,你可以用这个把一个对象变成一个 DotDict:
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