嵌套字典的类似对象的属性访问



我正在使用一个返回嵌套字典的包。当其他所有内容都在对象语法中时,使用字典语法在我的类方法中访问此返回对象感觉很尴尬。搜索将我带到了 bunch/neobunch 包,这似乎实现了我所追求的。我也看到了建议的命名元组,但这些不容易支持嵌套属性,大多数解决方案都依赖于在命名元组中使用字典进行嵌套。

实现这一目标

的更自然的方法是什么?

data = {'a': 'aval', 'b': {'b1':{'b2a':{'b3a':'b3aval','b3b':'b3bval'},'b2b':'b2bval'}} }
print(data['b']['b1']['b2a']['b3b'])  # dictionary access
# print(data.b.b1.b2a.b3b)  # desired access
import neobunch
data1 = neobunch.bunchify(data)
print(data1.b.b1.b2a.b3b)

下面的类可以让你做你想做的事(适用于Python 2和3(:

class AttrDict(dict):
    """ Dictionary subclass whose entries can be accessed by attributes (as well
        as normally).
    >>> obj = AttrDict()
    >>> obj['test'] = 'hi'
    >>> print obj.test
    hi
    >>> del obj.test
    >>> obj.test = 'bye'
    >>> print obj['test']
    bye
    >>> print len(obj)
    1
    >>> obj.clear()
    >>> print len(obj)
    0
    """
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self
    @classmethod
    def from_nested_dicts(cls, data):
        """ Construct nested AttrDicts from nested dictionaries. """
        if not isinstance(data, dict):
            return data
        else:
            return cls({key: cls.from_nested_dicts(data[key]) for key in data})

if __name__ == '__main__':
    data = {
        "a": "aval",
        "b": {
            "b1": {
                "b2b": "b2bval",
                "b2a": {
                    "b3a": "b3aval",
                    "b3b": "b3bval"
                }
            }
        }
    }
    attrdict = AttrDict.from_nested_dicts(data)
    print(attrdict.b.b1.b2a.b3b)  # -> b3bval

基于@martineau的出色答案,您可以使 AttrDict 类在不显式调用 from_nested_dict(( 函数的情况下处理嵌套字典:

class AttrDict(dict):
""" Dictionary subclass whose entries can be accessed by attributes
    (as well as normally).
"""
def __init__(self, *args, **kwargs):
    def from_nested_dict(data):
        """ Construct nested AttrDicts from nested dictionaries. """
        if not isinstance(data, dict):
            return data
        else:
            return AttrDict({key: from_nested_dict(data[key])
                                for key in data})
    super(AttrDict, self).__init__(*args, **kwargs)
    self.__dict__ = self
    for key in self.keys():
        self[key] = from_nested_dict(self[key])

json.loads有一个有趣的参数,称为object_hook,如果所有字典值都是JSON可序列化的,则可以使用该参数,即

import json
from types import SimpleNamespace
data = {'a': 'aval', 'b': {'b1':{'b2a':{'b3a':'b3aval','b3b':'b3bval'},'b2b':'b2bval'}}}
data1= json.loads(
    json.dumps(data), object_hook=lambda d: SimpleNamespace(**d)
)
print(data1.b.b1.b2a.b3b)  # -> b3bval

如果Guido在听,我认为SimpleNamespace应该采用一个recursive参数,这样你就可以做data1 = SimpleNamespace(recursive=True, **data)

尝试 Dotsi 或 EasyDict。它们都支持嵌套词典的点表示法。

>>> import dotsi
>>> data = dotsi.fy({'a': 'aval', 'b': {'b1':{'b2a':{'b3a':'b3aval','b3b':'b3bval'},'b2b':'b2bval'}} })
>>> print(data.b.b1.b2a.b3b)
b3bval
>>> 

除了dicts-within-dicts之外,Dotsi还支持dicts-in-lists-in-dicts.
注意:我是Dotsi的作者。

使用__setattr__方法怎么样?

>>> class AttrDict(dict):
...     def __getattr__(self, name):
...         if name in self:
...             return self[name]
... 
...     def __setattr__(self, name, value):
...         self[name] = self.from_nested_dict(value)
... 
...     def __delattr__(self, name):
...         if name in self:
...             del self[name]
... 
...     @staticmethod
...     def from_nested_dict(data):
...         """ Construct nested AttrDicts from nested dictionaries. """
...         if not isinstance(data, dict):
...             return data
...         else:
...             return AttrDict({key: AttrDict.from_nested_dict(data[key])
...                                 for key in data})
...         
>>> ad = AttrDict()
>>> ad
{}
>>> data = {'a': 'aval', 'b': {'b1':{'b2a':{'b3a':'b3aval','b3b':'b3bval'},'b2b':'b2bval'}} }
>>> ad.data = data
>>> ad.data
{'a': 'aval', 'b': {'b1': {'b2a': {'b3a': 'b3aval', 'b3b': 'b3bval'}, 'b2b': 'b2bval'}}}
>>> print(ad.data.b.b1.b2a.b3b)
    b3bval

可以使用基于基本对象构建的简单类:

class afoo1(object):
    def __init__(self, kwargs):
        for name in kwargs:
            val = kwargs[name]
            if isinstance(val, dict):
                val = afoo1(val)
            setattr(self,name,val)

我借用了argparse.Namespace定义,经过调整以允许嵌套。

它将用作

In [172]: dd={'a':'aval','b':{'b1':'bval'}}
In [173]: f=afoo1(dd)
In [174]: f
Out[174]: <__main__.afoo1 at 0xb3808ccc>
In [175]: f.a
Out[175]: 'aval'
In [176]: f.b
Out[176]: <__main__.afoo1 at 0xb380802c>
In [177]: f.b.b1
Out[177]: 'bval'

它也可以用**kwargs(连同*args(定义。 __repr__定义也可能很好。

与其他简单对象一样,可以添加属性,例如 f.c = f(递归定义(。 vars(f)返回字典,尽管它不进行任何递归转换(。

致谢:灵感来自@martineau的热门答案

还添加了对list/tuple的支持

class AttrDict(dict):
    """ support any nested structure """
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        for k, v in self.items():
            if isinstance(v, dict):
                self[k] = AttrDict(v)
            elif isinstance(v, (list, tuple)):
                self[k] = [AttrDict(_v) for _v in  v]
            elif isinstance(v, (int, float, bytes, bytearray, str)):
                self[k] = v
            else:
                raise NotImplementedError()
        self.__dict__ = self

去年我碰巧遇到了同样的问题。最后,我写了CHANfiG。

这里就是核心函数(注意DefaultDict的工作方式与collections.defaultdict类似(,属性样式访问的一些实现细节可能在FlatDict哪个是dict的子类。 __getattr____getitem__这两个调用都在内部get,但引发不同的Exceptionsetdelete也是如此。

class NestedDict(DefaultDict):
    r"""
    `NestedDict` further extends `DefaultDict` object by introducing a nested structure with `delimiter`.
    By default, `delimiter` is `.`, but it could be modified in subclass or by calling `dict.setattr('delimiter', D)`.
    `d = NestedDict({"a.b.c": 1})` is equivalent to `d = NestedDict({"a": {"b": {"c": 1}}})`,
    and you can access members either by `d["a.b.c"]` or more simply by `d.a.b.c`.
    This behavior allows you to pass keyword arguments to other function as easy as `func1(**d.func1)`.
    Since `NestedDict` inherits from `DefaultDict`, it also supports `default_factory`.
    With `default_factory`, you can assign `d.a.b.c = 1` without assign `d.a = NestedDict()` in the first place.
    Note that the constructor of `NestedDict` is different from `DefaultDict`, `default_factory` is not a positional
    argument, and must be set in a keyword argument.
    `NestedDict` also introduce `all_keys`, `all_values`, `all_items` methods to get all keys, values, items
    respectively in the nested structure.
    Attributes:
        convert_mapping: bool = False
            If `True`, all new values with a type of `Mapping` will be converted to `default_factory`.
                If `default_factory` is `Null`, will create an empty instance via `self.empty` as `default_factory`.
        delimiter: str = "."
            Delimiter for nested structure.
    Notes:
        When `convert_mapping` specified, all new values with type of `Mapping` will be converted to `default_factory`.
            If `default_factory` is `Null`, will create an empty instance via `self.empty` as `default_factory`.
        `convert_mapping` is automatically applied to arguments during initialisation.
    Examples:
        >>> NestedDict({"f.n": "chang"})
        NestedDict(
          ('f'): NestedDict(
            ('n'): 'chang'
          )
        )
        >>> d = NestedDict({"f.n": "chang"}, default_factory=NestedDict)
        >>> d.i.d = 1013
        >>> d['i.d']
        1013
        >>> d.i.d
        1013
        >>> d.dict()
        {'f': {'n': 'chang'}, 'i': {'d': 1013}}
    """
    convert_mapping: bool = False
    delimiter: str = "."
    def __init__(self, *args, default_factory: Optional[Callable] = None, **kwargs) -> None:
        super().__init__(default_factory, *args, **kwargs)
    def _init(self, *args, **kwargs) -> None:
        if len(args) == 1:
            args = args[0]
            if isinstance(args, Mapping):
                for key, value in args.items():
                    self.set(key, value, convert_mapping=True)
            elif isinstance(args, Iterable):
                for key, value in args:
                    self.set(key, value, convert_mapping=True)
        else:
            for key, value in args:
                self.set(key, value, convert_mapping=True)
        for key, value in kwargs.items():
            self.set(key, value, convert_mapping=True)
    def all_keys(self) -> Iterator:
        r"""
        Get all keys of `NestedDict`.
        Returns:
            (Iterator):
        Examples:
            >>> d = NestedDict({'a': 1, 'b': {'c': 2, 'd': 3}})
            >>> list(d.all_keys())
            ['a', 'b.c', 'b.d']
        """
        delimiter = self.getattr("delimiter", ".")
        @wraps(self.all_keys)
        def all_keys(self, prefix=""):
            for key, value in self.items():
                if prefix:
                    key = str(prefix) + str(delimiter) + str(key)
                if isinstance(value, NestedDict):
                    yield from all_keys(value, key)
                else:
                    yield key
        return all_keys(self)
    def all_values(self) -> Iterator:
        r"""
        Get all values of `NestedDict`.
        Returns:
            (Iterator):
        Examples:
            >>> d = NestedDict({'a': 1, 'b': {'c': 2, 'd': 3}})
            >>> list(d.all_values())
            [1, 2, 3]
        """
        for value in self.values():
            if isinstance(value, NestedDict):
                yield from value.all_values()
            else:
                yield value
    def all_items(self) -> Iterator[Tuple]:
        r"""
        Get all items of `NestedDict`.
        Returns:
            (Iterator):
        Examples:
            >>> d = NestedDict({'a': 1, 'b': {'c': 2, 'd': 3}})
            >>> list(d.all_items())
            [('a', 1), ('b.c', 2), ('b.d', 3)]
        """
        delimiter = self.getattr("delimiter", ".")
        @wraps(self.all_items)
        def all_items(self, prefix=""):
            for key, value in self.items():
                if prefix:
                    key = str(prefix) + str(delimiter) + str(key)
                if isinstance(value, NestedDict):
                    yield from all_items(value, key)
                else:
                    yield key, value
        return all_items(self)
    def get(self, name: Any, default: Any = Null) -> Any:
        r"""
        Get value from `NestedDict`.
        Note that `default` has higher priority than `default_factory`.
        Args:
            name:
            default:
        Returns:
            value:
                If `NestedDict` does not contain `name`, return `default`.
                If `default` is not specified, return `default_factory()`.
        Raises:
            KeyError: If `NestedDict` does not contain `name` and `default`/`default_factory` is not specified.
        Examples:
            >>> d = NestedDict({"i.d": 1013}, default_factory=NestedDict)
            >>> d.get('i.d')
            1013
            >>> d['i.d']
            1013
            >>> d.i.d
            1013
            >>> d.get('i.d', None)
            1013
            >>> d.get('f', 2)
            2
            >>> d.f
            NestedDict(<class 'chanfig.nested_dict.NestedDict'>, )
            >>> del d.f
            >>> d = NestedDict()
            >>> d.e
            Traceback (most recent call last):
            AttributeError: 'NestedDict' object has no attribute 'e'
            >>> d.e.f
            Traceback (most recent call last):
            AttributeError: 'NestedDict' object has no attribute 'e'
        """
        delimiter = self.getattr("delimiter", ".")
        try:
            while isinstance(name, str) and delimiter in name:
                name, rest = name.split(delimiter, 1)
                self, name = self[name], rest  # pylint: disable=W0642
        except (AttributeError, TypeError):
            raise KeyError(name) from None
        # if value is a python dict
        if not isinstance(self, NestedDict):
            if name not in self and default is not Null:
                return default
            return dict.get(self, name)
        return super().get(name, default)
    def set(  # pylint: disable=W0221
        self,
        name: Any,
        value: Any,
        convert_mapping: Optional[bool] = None,
    ) -> None:
        r"""
        Set value of `NestedDict`.
        Args:
            name:
            value:
            convert_mapping: Whether convert mapping to NestedDict.
                Defaults to self.convert_mapping.
        Examples:
            >>> d = NestedDict(default_factory=NestedDict)
            >>> d.set('i.d', 1013)
            >>> d.get('i.d')
            1013
            >>> d.dict()
            {'i': {'d': 1013}}
            >>> d['f.n'] = 'chang'
            >>> d.f.n
            'chang'
            >>> d.n.l = 'liu'
            >>> d['n.l']
            'liu'
            >>> d['f.n.e'] = "error"
            Traceback (most recent call last):
            ValueError: Cannot set `f.n.e` to `error`, as `f.n=chang`.
            >>> d['f.n.e.a'] = "error"
            Traceback (most recent call last):
            KeyError: 'e'
            >>> d.f.n.e.a = "error"
            Traceback (most recent call last):
            AttributeError: 'str' object has no attribute 'e'
            >>> d.setattr('convert_mapping', True)
            >>> d.a.b = {'c': {'d': 1}, 'e.f' : 2}
            >>> d.a.b.c.d
            1
            >>> d['c.d'] = {'c': {'d': 1}, 'e.f' : 2}
            >>> d.c.d['e.f']
            2
            >>> d.setattr('convert_mapping', False)
            >>> d.set('e.f', {'c': {'d': 1}, 'e.f' : 2}, convert_mapping=True)
            >>> d['e.f']['c.d']
            1
        """
        # pylint: disable=W0642
        full_name = name
        if convert_mapping is None:
            convert_mapping = self.convert_mapping
        delimiter = self.getattr("delimiter", ".")
        default_factory = self.getattr("default_factory", self.empty)
        try:
            while isinstance(name, str) and delimiter in name:
                name, rest = name.split(delimiter, 1)
                default_factory = self.getattr("default_factory", self.empty)
                if name in dir(self) and isinstance(getattr(self.__class__, name), property):
                    self, name = getattr(self, name), rest
                elif name not in self:
                    self, name = self.__missing__(name, default_factory()), rest
                else:
                    self, name = self[name], rest
        except (AttributeError, TypeError):
            raise KeyError(name) from None
        if convert_mapping and isinstance(value, Mapping):
            value = default_factory(value)
        if isinstance(self, Mapping):
            if not isinstance(self, NestedDict):
                dict.__setitem__(self, name, value)
            else:
                super().set(name, value)
        else:
            raise ValueError(
                f"Cannot set `{full_name}` to `{value}`, as `{delimiter.join(full_name.split(delimiter)[:-1])}={self}`."
            )
    def delete(self, name: Any) -> None:
        r"""
        Delete value from `NestedDict`.
        Args:
            name:
        Examples:
            >>> d = NestedDict({"i.d": 1013, "f.n": "chang"}, default_factory=NestedDict)
            >>> d.i.d
            1013
            >>> d.f.n
            'chang'
            >>> d.delete('i.d')
            >>> "i.d" in d
            False
            >>> d.i.d
            Traceback (most recent call last):
            AttributeError: 'NestedDict' object has no attribute 'd'
            >>> del d.f.n
            >>> d.f.n
            Traceback (most recent call last):
            AttributeError: 'NestedDict' object has no attribute 'n'
            >>> del d.e
            Traceback (most recent call last):
            AttributeError: 'NestedDict' object has no attribute 'e'
            >>> del d['e.f']
            Traceback (most recent call last):
            KeyError: 'f'
        """
        delimiter = self.getattr("delimiter", ".")
        try:
            while isinstance(name, str) and delimiter in name:
                name, rest = name.split(delimiter, 1)
                self, name = self[name], rest  # pylint: disable=W0642
        except (AttributeError, TypeError):
            raise KeyError(name) from None
        super().delete(name)
    def pop(self, name: Any, default: Any = Null) -> Any:
        r"""
        Pop value from `NestedDict`.
        Args:
            name:
            default:
        Returns:
            value: If `NestedDict` does not contain `name`, return `default`.
        Examples:
            >>> d = NestedDict({"i.d": 1013, "f.n": "chang", "n.a.b.c": 1}, default_factory=NestedDict)
            >>> d.pop('i.d')
            1013
            >>> d.pop('i.d', True)
            True
            >>> d.pop('i.d')
            Traceback (most recent call last):
            KeyError: 'd'
            >>> d.pop('e')
            Traceback (most recent call last):
            KeyError: 'e'
            >>> d.pop('e.f')
            Traceback (most recent call last):
            KeyError: 'f'
        """
        delimiter = self.getattr("delimiter", ".")
        try:
            while isinstance(name, str) and delimiter in name:
                name, rest = name.split(delimiter, 1)
                self, name = self[name], rest  # pylint: disable=W0642
        except (AttributeError, TypeError):
            raise KeyError(name) from None
        if not isinstance(self, dict) or name not in self:
            if default is not Null:
                return default
            raise KeyError(name)
        return super().pop(name)
    def __contains__(self, name: Any) -> bool:  # type: ignore
        delimiter = self.getattr("delimiter", ".")
        try:
            while isinstance(name, str) and delimiter in name:
                name, rest = name.split(delimiter, 1)
                self, name = self[name], rest  # pylint: disable=W0642
            return super().__contains__(name)
        except (TypeError, KeyError):  # TypeError when name is not in self
            return False

相关内容

  • 没有找到相关文章

最新更新