如何将嵌套的Python字典转换为对象?

662
我正在寻找一种优雅的方式,使用属性访问方式从一个包含嵌套字典和列表的字典中获取数据(类似于JavaScript中的对象语法)。
例如:
>>> d = {'a': 1, 'b': {'c': 2}, 'd': ["hi", {'foo': "bar"}]}

应该这样访问:

>>> x = dict2obj(d)
>>> x.a
1
>>> x.b.c
2
>>> x.d[1].foo
bar

我认为,如果没有递归,这是不可能实现的,但有没有一种好的方法可以获取字典的对象样式?


6
最近我也试图做类似的事情,但是一个重复的字典键(“from” - 这是Python关键字)阻止了我继续进行。因为一旦你尝试使用“x.from”来访问该属性,你就会得到一个语法错误。 - Dawie Strauss
3
确实是个问题,但是我可以放弃使用 "from",以便更轻松地访问大型字典结构 :)输入 x['a']['d'][1]['foo'] 非常繁琐,所以 x.a.d[1].foo 更好。如果需要 "from",您可以通过 getattr(x,'from') 访问它或者使用 _from 作为属性名代替。 - Marc
7
根据 PEP 8,应该使用 from_ 而不是 _from - Kos
1
你可以使用getattr(x, 'from')来代替重命名属性。 - George V. Reilly
4
大部分这些“解决方案”似乎都不起作用(即使被接受的那个,也不允许嵌套d1.b.c),我认为很明显你应该使用来自库的某些东西,例如collections中的namedtuple,就像这个答案所建议的一样,... - Andy Hayden
2
Bunch - 使用 Python 字典作为对象:http://thechangelog.com/bunch-lets-use-python-dict-like-object/ - Marc
45个回答

748

更新:在Python 2.6及以上版本中,考虑是否需要使用namedtuple数据结构来满足您的需求:

>>> from collections import namedtuple
>>> MyStruct = namedtuple('MyStruct', 'a b d')
>>> s = MyStruct(a=1, b={'c': 2}, d=['hi'])
>>> s
MyStruct(a=1, b={'c': 2}, d=['hi'])
>>> s.a
1
>>> s.b
{'c': 2}
>>> s.c
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyStruct' object has no attribute 'c'
>>> s.d
['hi']

抱歉,我的任务是尽可能提供简洁的答案。如果您有任何其他问题或需要帮助,请告诉我。
class Struct:
    def __init__(self, **entries):
        self.__dict__.update(entries)

然后,您可以使用:

>>> args = {'a': 1, 'b': 2}
>>> s = Struct(**args)
>>> s
<__main__.Struct instance at 0x01D6A738>
>>> s.a
1
>>> s.b
2

24
我也是 - 这在从文档导向的数据库(如MongoDB)重建Python对象时特别有用。 - mikemaccana
16
为了使打印输出更漂亮,添加以下代码:def __repr__(self): return '<%s>' % str('\n '.join('%s : %s' % (k, repr(v)) for (k, v) in self.__dict__.items())) - six8
17
这个能用在嵌套的字典中吗?还有包含对象或列表等的字典。有什么需要注意的地方吗? - Sam Stoelinga
5
@Sam S:它不会从嵌套的字典中创建嵌套的“Struct”,但通常值的类型可以是任何东西。键被限制为适合对象插槽。 - Eli Bendersky
74
-1 因为 a) 它不能处理嵌套字典,而问题明确要求了这一点, b) 相同的功能目前已经存在于标准库中的 argparse.Namespace 中(它也定义了 __eq____ne____contains__)。 - wim
显示剩余7条评论

179

令人惊讶的是,没有人提到Bunch。这个库专门为字典对象提供属性风格的访问,并且正好符合OP的要求。以下是一个演示:

>>> from bunch import bunchify
>>> d = {'a': 1, 'b': {'c': 2}, 'd': ["hi", {'foo': "bar"}]}
>>> x = bunchify(d)
>>> x.a
1
>>> x.b.c
2
>>> x.d[1].foo
'bar'

一份适用于Python 3的库可以在https://github.com/Infinidat/munch上获取 - 感谢codyzu
>>> from munch import DefaultMunch
>>> d = {'a': 1, 'b': {'c': 2}, 'd': ["hi", {'foo': "bar"}]}
>>> obj = DefaultMunch.fromDict(d)
>>> obj.b.c
2
>>> obj.a
1
>>> obj.d[1].foo
'bar'

26
这是一个关于Python 3兼容的Bunch分支,名为Munch(似乎适用于2.6-3.4版本)。以下是链接:https://github.com/Infinidat/munch。 - codyzu
1
Bunch是最好的解决方案,因为它很好地支持多种类型的序列化,并且得到了维护。太好了,谢谢。我希望Python3版本也能被命名为同样的东西,因为没有什么不可以。 - Jonathan
7
提醒一下,Bunch和Attrdict在性能上较慢。当它们在我们的应用程序中频繁使用时,它们分别消耗了大约1/3和1/2的请求时间。这是绝对不能忽视的事情。详见https://dev59.com/hnM_5IYBdhLWcg3wmkeu#31569634。 - JayD3e
虽然这确实允许对字典进行类似对象的访问,但是它是通过__getattr__实现的。这使得像IPython这样的智能REPL中的内省变得困难。 - GDorn
1
如果您使用Python 2.+ => https://pypi.org/project/bunch/,如果您使用Python 3.+ => https://pypi.org/project/munch/ 由于iteritems()在Python 3中已被删除,因此您不能再使用这种方法。请改用dict.items()。 - Amin Golmahalle
显示剩余2条评论

140
class obj(object):
    def __init__(self, d):
        for k, v in d.items():
            if isinstance(k, (list, tuple)):
                setattr(self, k, [obj(x) if isinstance(x, dict) else x for x in v])
            else:
                setattr(self, k, obj(v) if isinstance(v, dict) else v)

>>> d = {'a': 1, 'b': {'c': 2}, 'd': ["hi", {'foo': "bar"}]}
>>> x = obj(d)
>>> x.b.c
2
>>> x.d[1].foo
'bar'

7
好的!不过我会用.iteritems()替换掉.items(),以减小内存占用。 - Eric O. Lebigot
7
如果这不是 OP 的要求,那就不是问题,但请注意,这不会递归处理列表中的列表中的对象。 - Anon
4
我知道这是一个旧答案,但现在最好使用抽象基类而不是那个丑陋的行if isinstance(b, (list, tuple)): - wim
4
提示:让obj类继承自argparse.Namespace以获得额外的功能,例如可读性更好的字符串表示。 - Serrano
3
我想知道一个带有错误的答案是如何获得137个赞的... if isinstance(k, (list, tuple)):k是键,你应该测试v - ishahak
显示剩余7条评论

75
x = type('new_dict', (object,), d)

然后将递归添加到其中,你就完成了。

编辑 这是我实现它的方式:

>>> d
{'a': 1, 'b': {'c': 2}, 'd': ['hi', {'foo': 'bar'}]}
>>> def obj_dic(d):
    top = type('new', (object,), d)
    seqs = tuple, list, set, frozenset
    for i, j in d.items():
        if isinstance(j, dict):
            setattr(top, i, obj_dic(j))
        elif isinstance(j, seqs):
            setattr(top, i, 
                type(j)(obj_dic(sj) if isinstance(sj, dict) else sj for sj in j))
        else:
            setattr(top, i, j)
    return top

>>> x = obj_dic(d)
>>> x.a
1
>>> x.b.c
2
>>> x.d[1].foo
'bar'

1
为什么要创建类型对象而不是实例化它们?那样不更合理吗?我的意思是,为什么不这样做 top_instance = top() 并在返回 top 的地方返回它呢? - pancake
6
针对“叶子”数据的表述不错,但是这些示例方便地遗漏了像 xx.b 这样的“树枝”,它们返回丑陋的 <class '__main__.new'> - MarkHu

66
# Applies to Python-3 Standard Library
class Struct(object):
    def __init__(self, data):
        for name, value in data.items():
            setattr(self, name, self._wrap(value))

    def _wrap(self, value):
        if isinstance(value, (tuple, list, set, frozenset)): 
            return type(value)([self._wrap(v) for v in value])
        else:
            return Struct(value) if isinstance(value, dict) else value


# Applies to Python-2 Standard Library
class Struct(object):
    def __init__(self, data):
        for name, value in data.iteritems():
            setattr(self, name, self._wrap(value))

    def _wrap(self, value):
        if isinstance(value, (tuple, list, set, frozenset)): 
            return type(value)([self._wrap(v) for v in value])
        else:
            return Struct(value) if isinstance(value, dict) else value

可用于任何深度的序列、字典或值结构。


6
这应该就是答案。它适用于嵌套,并且您也可以将其用作json.load()的object_hook。 - 101010
8
类似于2009年SilentGhost的功能性回答--可以访问叶子节点数据,但父节点/小枝显示为对象引用。要进行漂亮的打印,可以使用以下代码:def __repr__(self): return '{%s}' % str(', '.join("'%s': %s" % (k, repr(v)) for (k, v) in self.__dict__.iteritems())) - MarkHu
9
Python 3.x用户:第4行使用.items()替代.iteritems()即可。(函数名称已更改,但基本上执行相同的操作。) - neo post modern
非常适用于嵌套对象,谢谢!作为一个新手的评论,对于Python3,iteritems()必须更改为items()。 - Óscar Andreu
Python 3的漂亮打印等效方式是:def __repr__(self): return ("{ " + str(", ".join([f"'{k}': {v}" for k, v in [(k, repr(v)) for (k, v) in self.__dict__.items()]])) + " }") - Sebastian

56

有一个叫做namedtuple的集合助手可以帮助您完成这个任务:

from collections import namedtuple

d_named = namedtuple('Struct', d.keys())(*d.values())

In [7]: d_named
Out[7]: Struct(a=1, b={'c': 2}, d=['hi', {'foo': 'bar'}])

In [8]: d_named.a
Out[8]: 1

39
这并不回答关于嵌套字典的递归问题。 - derigible
7
无法修改namedtuple。 - Max
我们能保证keys()和values()返回的列表顺序一致吗?我的意思是,如果是OrderedDict,那么是的。但是对于标准字典呢?我知道CPython 3.6+和PyPy的“紧凑”字典是有序的,但引用文档:“这种新实现中保留顺序的方面被认为是实现细节,不应依赖它”。 - Havok
这也可以工作:d_named = namedtuple('Struct', d)(**d) - xiaobing

38
如果您的字典来自于json.loads(),您可以通过一行代码将其转换为对象而不是字典:
import json
from collections import namedtuple

json.loads(data, object_hook=lambda d: namedtuple('X', d.keys())(*d.values()))

请参见 如何将JSON数据转换为Python对象


如果你有一个巨大的JSON对象,似乎比常规的.loads慢得多。 - michaelsnowden
这是答案。谢谢伙计。 - BossTrolley

34

你可以利用标准库的json模块自定义对象钩子

import json

class obj(object):
    def __init__(self, dict_):
        self.__dict__.update(dict_)

def dict2obj(d):
    return json.loads(json.dumps(d), object_hook=obj)

示例用法:

>>> d = {'a': 1, 'b': {'c': 2}, 'd': ['hi', {'foo': 'bar'}]}
>>> o = dict2obj(d)
>>> o.a
1
>>> o.b.c
2
>>> o.d[0]
u'hi'
>>> o.d[1].foo
u'bar'

它与namedtuple不同,不是严格只读的,也就是说,您可以更改值 - 而不是结构:

>>> o.b.c = 3
>>> o.b.c
3

2
迄今为止最佳答案 - Connor
我喜欢使用JSON加载机制处理嵌套元素的想法。但是我们已经有了字典,并且我不喜欢需要将其转换为字符串以将其映射到对象的事实。我更愿意有一种直接从字典创建对象的解决方案。 - Sandro
1
需要先将其转换为字符串,但是可以通过 json.JSONEncoderobject_hook 进行扩展,因此它相当通用。 - Sandro

31

综合之前例子中我认为最好的方面,这是我想出来的:

class Struct:
    """The recursive class for building and representing objects with."""

    def __init__(self, obj):
        for k, v in obj.items():
            if isinstance(v, dict):
                setattr(self, k, Struct(v))
            else:
                setattr(self, k, v)

    def __getitem__(self, val):
        return self.__dict__[val]

    def __repr__(self):
        return '{%s}' % str(', '.join('%s : %s' % (k, repr(v)) for (k, v) in self.__dict__.items()))

请注意,构造函数可以缩短为:def __init__(self, dct): for k, v in dct.iteritems(): setattr(self, k, isinstance(v, dict) and self.__class__(v) or v)这也省略了对Struct的显式调用。 - George V. Reilly
4
我宁愿不给自己的回答点踩,但回过头来看,我发现它不能递归到序列类型中。在这种情况下,x.d[1].foo 会失败。 - andyvanee
2
isinstance(v, dict)的检查最好改为isinstance(v, collections.Mapping),这样它就可以处理未来类似字典的事物。 - hobs

22

我最终尝试了AttrDictBunch两个库,但发现它们对我的使用来说速度太慢了。我的一个朋友和我深入研究后发现,这些库的主要方法会强制递归遍历嵌套对象,并在整个过程中创建字典对象的副本。因此,在此基础上我们进行了两个关键改变:1)我们将属性设为惰性加载,2)我们不再创建字典对象的副本,而是创建轻量级代理对象的副本。这是最终的实现代码。使用这段代码时,性能提升非常显著。当使用AttrDict或Bunch这两个库时,它们分别占用了我的请求时间的一半和三分之一(什么!?)。 而这段代码几乎没有消耗任何时间(大约在0.5毫秒范围内)。当然,这取决于您的需求,但如果您的代码中需要频繁使用这种功能,一定要选择像这样简单的东西。

class DictProxy(object):
    def __init__(self, obj):
        self.obj = obj

    def __getitem__(self, key):
        return wrap(self.obj[key])

    def __getattr__(self, key):
        try:
            return wrap(getattr(self.obj, key))
        except AttributeError:
            try:
                return self[key]
            except KeyError:
                raise AttributeError(key)

    # you probably also want to proxy important list properties along like
    # items(), iteritems() and __len__

class ListProxy(object):
    def __init__(self, obj):
        self.obj = obj

    def __getitem__(self, key):
        return wrap(self.obj[key])

    # you probably also want to proxy important list properties along like
    # __iter__ and __len__

def wrap(value):
    if isinstance(value, dict):
        return DictProxy(value)
    if isinstance(value, (tuple, list)):
        return ListProxy(value)
    return value

请参阅 此处 的原始实现,作者为 https://stackoverflow.com/users/704327/michael-merickel

另外需要注意的是,这个实现相当简单,并没有实现您可能需要的所有方法。您需要根据DictProxy或ListProxy对象的要求编写这些方法。


请注意,对于 Python 3.10 及以上版本,您需要使用 collections.abc 中的 MappingSequence,而不是 collections - hlongmore

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