Python:轻松访问深层嵌套的dict(get和set)

时间:2010-09-26 13:23:28

标签: python

我正在构建一些Python代码来读取和操作深层嵌套的dicts(最终用于与JSON服务进行交互,但是出于其他目的它会很棒)我正在寻找一种方便的读取/设置/更新dict中的值很深,不需要很多代码。

@see Python: Recursively access dict via attributes as well as index access? - Curt Hagenlocher的“DotDictify”解决方案非常有说服力。我也喜欢Ben Alman在http://benalman.com/projects/jquery-getobject-plugin/中为JavaScript提供的内容。以某种方式将两者结合起来会很棒。

在Curt Hagenlocher和Ben Alman的例子的基础上,拥有像以下这样的能力在Python中会很棒:

>>> my_obj = DotDictify()
>>> my_obj.a.b.c = {'d':1, 'e':2}
>>> print my_obj
{'a': {'b': {'c': {'d': 1, 'e': 2}}}}
>>> print my_obj.a.b.c.d
1
>>> print my_obj.a.b.c.x
None
>>> print my_obj.a.b.c.d.x
None
>>> print my_obj.a.b.c.d.x.y.z
None

有任何想法是否可行,如果可行,如何修改DotDictify解决方案?

或者,可以使get方法接受点符号(并添加补充集方法)但是对象符号确定更清晰。

>>> my_obj = DotDictify()
>>> my_obj.set('a.b.c', {'d':1, 'e':2})
>>> print my_obj
{'a': {'b': {'c': {'d': 1, 'e': 2}}}}
>>> print my_obj.get('a.b.c.d')
1
>>> print my_obj.get('a.b.c.x')
None
>>> print my_obj.get('a.b.c.d.x')
None
>>> print my_obj.get('a.b.c.d.x.y.z')
None

这种类型的交互对于处理深层嵌套的dicts非常有用。有人知道另一种策略(或示例代码片段/库)吗?

4 个答案:

答案 0 :(得分:33)

属性树

你的第一个规范的问题是Python无法在__getitem__中告诉你,在my_obj.a.b.c.d,你是否会在下一个不存在的树上继续前进,在这种情况下它需要返回一个对象使用__getitem__方法,这样您就不会被AttributeError抛出,或者如果您想要一个值,在这种情况下它需要返回None

我认为,在上面的每一个案例中,你应该期望它抛出KeyError而不是返回None。原因是您无法判断None是否表示“无密钥”或“某人实际存储None在该位置”。对于此行为,您只需执行dotdictify,删除marker,然后将__getitem__替换为:

def __getitem__(self, key):
    return self[key]

因为您真正想要的是dict __getattr____setattr__

可能有一种方法可以完全删除__getitem__并说__getattr__ = dict.__getitem__之类的内容,但我认为这可能会过度优化,如果您以后决定要{{1像__getitem__原来那样创建树,在这种情况下你可以将它改为:

dotdictify

我不喜欢原始def __getitem__(self, key): if key not in self: dict.__setitem__(self, key, dotdictify()) return dict.__getitem__(self, key) 中的marker商家。

路径支持

第二个规范(覆盖dotdictifyget())是正常的set()有一个dict,其运作方式与您描述的不同,甚至没有get()(虽然它有一个set,它是setdefault()的逆操作。人们期望get()采用两个参数,如果找不到密钥则第二个是默认参数。

如果您想扩展get__getitem__以处理虚线键表示法,则需要将__setitem__修改为:

doctictify

测试代码:

class dotdictify(dict):
    def __init__(self, value=None):
        if value is None:
            pass
        elif isinstance(value, dict):
            for key in value:
                self.__setitem__(key, value[key])
        else:
            raise TypeError, 'expected dict'

    def __setitem__(self, key, value):
        if '.' in key:
            myKey, restOfKey = key.split('.', 1)
            target = self.setdefault(myKey, dotdictify())
            if not isinstance(target, dotdictify):
                raise KeyError, 'cannot set "%s" in "%s" (%s)' % (restOfKey, myKey, repr(target))
            target[restOfKey] = value
        else:
            if isinstance(value, dict) and not isinstance(value, dotdictify):
                value = dotdictify(value)
            dict.__setitem__(self, key, value)

    def __getitem__(self, key):
        if '.' not in key:
            return dict.__getitem__(self, key)
        myKey, restOfKey = key.split('.', 1)
        target = dict.__getitem__(self, myKey)
        if not isinstance(target, dotdictify):
            raise KeyError, 'cannot get "%s" in "%s" (%s)' % (restOfKey, myKey, repr(target))
        return target[restOfKey]

    def __contains__(self, key):
        if '.' not in key:
            return dict.__contains__(self, key)
        myKey, restOfKey = key.split('.', 1)
        target = dict.__getitem__(self, myKey)
        if not isinstance(target, dotdictify):
            return False
        return restOfKey in target

    def setdefault(self, key, default):
        if key not in self:
            self[key] = default
        return self[key]

    __setattr__ = __setitem__
    __getattr__ = __getitem__

答案 1 :(得分:2)

我曾经使用类似的东西为应用程序构建类似Trie的东西。我希望它有所帮助。

class Trie:
    """
    A Trie is like a dictionary in that it maps keys to values.
    However, because of the way keys are stored, it allows
    look up based on the longest prefix that matches.

    """

    def __init__(self):
        # Every node consists of a list with two position.  In
        # the first one,there is the value while on the second
        # one a dictionary which leads to the rest of the nodes.
        self.root = [0, {}]


    def insert(self, key):
        """
        Add the given value for the given key.

        >>> a = Trie()
        >>> a.insert('kalo')
        >>> print(a)
        [0, {'k': [1, {'a': [1, {'l': [1, {'o': [1, {}]}]}]}]}]
        >>> a.insert('kalo')
        >>> print(a)
        [0, {'k': [2, {'a': [2, {'l': [2, {'o': [2, {}]}]}]}]}]
        >>> b = Trie()
        >>> b.insert('heh')
        >>> b.insert('ha')
        >>> print(b)
        [0, {'h': [2, {'a': [1, {}], 'e': [1, {'h': [1, {}]}]}]}]

        """

        # find the node to append the new value.
        curr_node = self.root
        for k in key:
            curr_node = curr_node[1].setdefault(k, [0, {}])
            curr_node[0] += 1


    def find(self, key):
        """
        Return the value for the given key or None if key not
        found.

        >>> a = Trie()
        >>> a.insert('ha')
        >>> a.insert('ha')
        >>> a.insert('he')
        >>> a.insert('ho')
        >>> print(a.find('h'))
        4
        >>> print(a.find('ha'))
        2
        >>> print(a.find('he'))
        1

        """

        curr_node = self.root
        for k in key:
            try:
                curr_node = curr_node[1][k]
            except KeyError:
                return 0
        return curr_node[0]

    def __str__(self):
        return str(self.root)

    def __getitem__(self, key):
        curr_node = self.root
        for k in key:
            try:
                curr_node = curr_node[1][k]
            except KeyError:
                yield None
        for k in curr_node[1]:
            yield k, curr_node[1][k][0]

if __name__ == '__main__':
    a = Trie()
    a.insert('kalo')
    a.insert('kala')
    a.insert('kal')
    a.insert('kata')
    print(a.find('kala'))
    for b in a['ka']:
        print(b)
    print(a)

答案 2 :(得分:2)

对于其他googlers:我们现在有addict

pip install addict

mapping.a.b.c.d.e = 2
mapping
{'a': {'b': {'c': {'d': {'e': 2}}}}}

我广泛使用它。

为了使用虚线路径,我找到了dotted

obj = DottedDict({'hello': {'world': {'wide': 'web'}}})
obj['hello.world.wide'] == 'web'  # true

答案 3 :(得分:1)

较早的答案中有一些不错的技巧,但是它们都需要用自定义的替换标准的Python数据结构(字典等),并且不适用于不是有效属性名称的键。

这几天,我们可以使用为此目的而构建的,纯Python,Python 2/3兼容的库glom来做得更好。以您的示例为例:

import glom

target = {}  # a plain dictionary we will deeply set on
glom.assign(target, 'a.b.c', {'d': 1, 'e': 2}, missing=dict)
# {'a': {'b': {'c': {'e': 2, 'd': 1}}}}

注意missing=dict,用于自动创建字典。我们可以使用glom的deep-get轻松获得价值:

glom.glom(target, 'a.b.c.d')
# 1

glom可以做很多事情,尤其是在深入获取和设置方面。我应该知道,因为(完全披露)我创建了它。这意味着如果您发现差距,则应该let me know