递归DotDict

10

我有一个实用类,可以使Python字典在获取和设置属性方面的行为与JavaScript对象类似。

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__

我希望它也能将嵌套字典转换为DotDict()实例。我希望能够通过__init____new__来完成这样的操作,但是我还没有想出任何可行的方法:

def __init__(self, dct):
    for key in dct.keys():
        if hasattr(dct[key], 'keys'):
            dct[key] = DotDict(dct[key])

我该如何递归地将嵌套字典转换为DotDict()实例?

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

你正在替换 dct 中的值,这是您传入的原始字典。新对象是原始对象的副本,因此它保留了原始值。如果您替换 self[key],它应该可以工作。 - Thomas K
这看起来像是一个重复的问题,与https://dev59.com/mnA75IYBdhLWcg3w7tt6相同。 - Rahul Jha
4个回答

15

我不知道你在构造函数中复制值的地方在哪里。由于这个原因,DotDict始终为空。当我添加键赋值时,它起作用了:

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

看起来有点危险和容易出错,更不用说会给其他开发人员带来无数惊喜的源头,但似乎是可以工作的。


这看起来很好,只需要添加一个默认值 dct={} ,这样它就可以像这样使用:__d = DotDict()__。 - tponthieux
有没有人对如何更好地确保嵌套值是某种字典有什么建议?我希望它能够支持任何像有序字典这样的东西,因此我不一定想检查它是否是字典实例。 - tponthieux
为什么不检查 __getitem__ 而是 keys - BoppreH
甚至可以使用 isinstance(value, dict) - Eric
isinstance(value, collections.Mapping) 是否符合要求?(双关语) - Eric
显示剩余3条评论

5
< p >< em >毫不羞耻地宣传我自己的软件包 < p>有一个软件包正好可以做你想要的事情,而且还能做更多的事情,它叫做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:我是Prodict的作者。

PS 2:这是另一个问题答案的直接复制粘贴。


无法处理嵌套对象列表。 - Fedor Petrov

2

我对于这个问题找到的各种答案都不是很满意。我的实现目标是: 1)尽可能少地创建新对象属性。 2)不允许覆盖内置属性的访问权限。 3)该类将添加的项转换为保持一致性。

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

2

被接受的答案存在一些问题,例如无法处理 hasattr()。使用键模拟属性意味着你需要做更多的事情,而不仅仅是赋值 __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

不支持嵌套的字典对象列表。 - MikeL
很确定它会这样做。这就是为什么__init__循环遍历值并分配嵌套的DotDict对象。 - mattmc3
有一个打字错误。在 __init__ 中应该是 val = DotDict(val) - tfeldmann
@Thomas - 发现得很好,但错误实际上出现在下一行。永远不要更改迭代变量。无论如何,我已经修复了它。 - mattmc3
@mattmc3 是的,那样更好。谢谢! - tfeldmann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接