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

mbyulnm0  于 2023-05-21  发布在  Python
关注(0)|答案(8)|浏览(161)

我使用了一个返回嵌套字典的包。在我的类方法中使用字典语法访问这个返回对象感觉很尴尬,而其他所有内容都是对象语法。搜索把我带到了bunch / neobunch包,它似乎实现了我所追求的。我也看到namedtuple的建议,但这些不容易支持嵌套属性,大多数解决方案依赖于使用namedtuple内的字典嵌套。
有什么更自然的方式来实现这一点?

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)
ecfsfe2w

ecfsfe2w1#

下面的类可以让你做你想做的事情(适用于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
iqxoj9l9

iqxoj9l92#

基于@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])
68de4m5k

68de4m5k3#

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)了。

gc0ot86w

gc0ot86w4#

尝试DotsiEasyDict。它们都支持嵌套字典的点表示法。

>>> 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-within-lists-within-dicts。
注:我是Dotsi的作者。

smtd7mpg

smtd7mpg5#

如何使用__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
5jdjgkvh

5jdjgkvh6#

一个简单的类,构建在基本对象上,可以使用:

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)返回一个字典,尽管它不做任何递归转换)。

brc7rcf0

brc7rcf07#

致谢:灵感来自@martineau的top answer
还添加了对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
n3ipq98p

n3ipq98p8#

去年我也遇到了同样的问题。最后,我写了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

相关问题