我正在使用一个返回嵌套字典的包。当其他所有内容都在对象语法中时,使用字典语法在我的类方法中访问此返回对象感觉很尴尬。搜索将我带到了 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
,但引发不同的Exception
,set
和delete
也是如此。
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