如何最佳实现嵌套字典?

228

我有一个数据结构,它基本上相当于一个嵌套的字典。假设它看起来像这样:

{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

现在,维护和创建这个东西相当痛苦;每次我有一个新的州/县/行业,我都必须通过令人讨厌的try/catch块创建下层字典。此外,如果我想遍历所有值,我还必须创建令人恼火的嵌套迭代器。

我也可以使用元组作为键,如下所示:

{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'salesmen'): 62,
 ('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

这使得对值进行迭代非常简单和自然,但是在执行聚合操作和查看字典子集(例如,如果我只想逐个州地浏览)等操作时,语法上更加繁琐。

基本上,有时我想将嵌套的字典视为平面字典,有时我确实想将其视为复杂层次结构。我可以将所有内容包装在一个类中,但似乎已经有人做过了。或者,似乎有一些非常优雅的语法结构可以实现这一点。

我该如何更好地做到这一点?

补充说明:我知道setdefault(),但它的语法并不清晰。此外,您创建的每个子字典仍需要手动设置setdefault()

22个回答

5

collections.defaultdict可以被子类化以创建一个嵌套字典。然后在该类中添加任何有用的迭代方法。

>>> from collections import defaultdict
>>> class nesteddict(defaultdict):
    def __init__(self):
        defaultdict.__init__(self, nesteddict)
    def walk(self):
        for key, value in self.iteritems():
            if isinstance(value, nesteddict):
                for tup in value.walk():
                    yield (key,) + tup
            else:
                yield key, value


>>> nd = nesteddict()
>>> nd['new jersey']['mercer county']['plumbers'] = 3
>>> nd['new jersey']['mercer county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['salesmen'] = 62
>>> nd['new york']['queens county']['plumbers'] = 9
>>> nd['new york']['queens county']['salesmen'] = 36
>>> for tup in nd.walk():
    print tup


('new jersey', 'mercer county', 'programmers', 81)
('new jersey', 'mercer county', 'plumbers', 3)
('new jersey', 'middlesex county', 'programmers', 81)
('new jersey', 'middlesex county', 'salesmen', 62)
('new york', 'queens county', 'salesmen', 36)
('new york', 'queens county', 'plumbers', 9)

1
这是最接近我所寻找的答案。但理想情况下,会有各种辅助函数,例如walk_keys()之类的函数。我很惊讶标准库中没有做到这一点的东西。 - YGA

4

defaultdict()是你的好朋友!

如果你需要一个二维字典,可以这样实现:

d = defaultdict(defaultdict)
d[1][2] = 3

对于更多的维度,您可以:

d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4

这个答案最多只适用于三个级别。对于任意级别,请考虑这个答案 - Asclepius

4
至于“令人讨厌的try/catch块”:
d = {}
d.setdefault('key',{}).setdefault('inner key',{})['inner inner key'] = 'value'
print d

产量
{'key': {'inner key': {'inner inner key': 'value'}}}

你可以使用这个工具将你的平面字典格式转换为结构化格式:
fd = {('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'programmers'): 81,
 ('new jersey', 'middlesex county', 'salesmen'): 62,
 ('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

for (k1,k2,k3), v in fd.iteritems():
    d.setdefault(k1, {}).setdefault(k2, {})[k3] = v

3

要轻松迭代嵌套字典,何不编写简单的生成器呢?

def each_job(my_dict):
    for state, a in my_dict.items():
        for county, b in a.items():
            for job, value in b.items():
                yield {
                    'state'  : state,
                    'county' : county,
                    'job'    : job,
                    'value'  : value
                }

那么,如果你有一个复杂的嵌套字典,迭代它就变得简单了:
for r in each_job(my_dict):
    print "There are %d %s in %s, %s" % (r['value'], r['job'], r['county'], r['state'])

显然,您的生成器可以产生任何对您有用的数据格式。

为什么要使用try catch块来读取树?在尝试检索之前,查询字典中是否存在键可能更容易(并且可能更安全)。使用守卫子句的函数可能如下所示:

if not my_dict.has_key('new jersey'):
    return False

nj_dict = my_dict['new jersey']
...

或者,也可以使用get方法,可能有点啰嗦:
value = my_dict.get('new jersey', {}).get('middlesex county', {}).get('salesmen', 0)

如果你想更简洁,可以考虑使用collections.defaultdict,这是Python 2.5以来标准库的一部分,它能提高效率。

import collections

def state_struct(): return collections.defaultdict(county_struct)
def county_struct(): return collections.defaultdict(job_struct)
def job_struct(): return 0

my_dict = collections.defaultdict(state_struct)

print my_dict['new jersey']['middlesex county']['salesmen']

在这里,我对您的数据结构的含义进行了假设,但应该很容易调整以适应您实际想要做的事情。


2

我认为将这个内容封装在一个类中,并实现__getitem____setitem__方法,以便实现一个简单的查询语言是个好主意:

>>> d['new jersey/mercer county/plumbers'] = 3
>>> d['new jersey/mercer county/programmers'] = 81
>>> d['new jersey/mercer county/programmers']
81
>>> d['new jersey/mercer country']
<view which implicitly adds 'new jersey/mercer county' to queries/mutations>

如果你想更加高级一些,你也可以实现类似这样的功能:
>>> d['*/*/programmers']
<view which would contain 'programmers' entries>

但我认为这样的事情实施起来会非常有趣 :D


我认为这是一个不好的想法 - 你永远无法预测键的语法。你仍然需要覆盖__getitem__和__setitem__,但让它们接受元组。 - YGA
3
你可能是对的,但是想着实现这样的迷你语言也是很有趣的。 - Aaron Maenpaa

1
class JobDb(object):
    def __init__(self):
        self.data = []
        self.all = set()
        self.free = []
        self.index1 = {}
        self.index2 = {}
        self.index3 = {}

    def _indices(self,(key1,key2,key3)):
        indices = self.all.copy()
        wild = False
        for index,key in ((self.index1,key1),(self.index2,key2),
                                             (self.index3,key3)):
            if key is not None:
                indices &= index.setdefault(key,set())
            else:
                wild = True
        return indices, wild

    def __getitem__(self,key):
        indices, wild = self._indices(key)
        if wild:
            return dict(self.data[i] for i in indices)
        else:
            values = [self.data[i][-1] for i in indices]
            if values:
                return values[0]

    def __setitem__(self,key,value):
        indices, wild = self._indices(key)
        if indices:
            for i in indices:
                self.data[i] = key,value
        elif wild:
            raise KeyError(k)
        else:
            if self.free:
                index = self.free.pop(0)
                self.data[index] = key,value
            else:
                index = len(self.data)
                self.data.append((key,value))
                self.all.add(index)
            self.index1.setdefault(key[0],set()).add(index)
            self.index2.setdefault(key[1],set()).add(index)
            self.index3.setdefault(key[2],set()).add(index)

    def __delitem__(self,key):
        indices,wild = self._indices(key)
        if not indices:
            raise KeyError
        self.index1[key[0]] -= indices
        self.index2[key[1]] -= indices
        self.index3[key[2]] -= indices
        self.all -= indices
        for i in indices:
            self.data[i] = None
        self.free.extend(indices)

    def __len__(self):
        return len(self.all)

    def __iter__(self):
        for key,value in self.data:
            yield key

例子:

>>> db = JobDb()
>>> db['new jersey', 'mercer county', 'plumbers'] = 3
>>> db['new jersey', 'mercer county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'salesmen'] = 62
>>> db['new york', 'queens county', 'plumbers'] = 9
>>> db['new york', 'queens county', 'salesmen'] = 36

>>> db['new york', None, None]
{('new york', 'queens county', 'plumbers'): 9,
 ('new york', 'queens county', 'salesmen'): 36}

>>> db[None, None, 'plumbers']
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new york', 'queens county', 'plumbers'): 9}

>>> db['new jersey', 'mercer county', None]
{('new jersey', 'mercer county', 'plumbers'): 3,
 ('new jersey', 'mercer county', 'programmers'): 81}

>>> db['new jersey', 'middlesex county', 'programmers']
81

>>>

编辑:现在使用通配符(None)查询时返回字典,否则返回单个值。


为什么要返回列表?似乎应该返回一个字典(这样你就知道每个数字代表什么)或者一个总和(因为这是你真正可以用列表做的事情)。 - Ben Blank

1

除非你的数据集比较小,否则你可能需要考虑使用关系型数据库。它将完全满足你的需求:轻松添加计数、选择计数子集,甚至按州、县、职业或任何这些组合来聚合计数。


0

你可以在lambda和defaultdict中使用递归,无需定义名称:

a = defaultdict((lambda f: f(f))(lambda g: lambda:defaultdict(g(g))))

这里有一个例子:

>>> a['new jersey']['mercer county']['plumbers']=3
>>> a['new jersey']['middlesex county']['programmers']=81
>>> a['new jersey']['mercer county']['programmers']=81
>>> a['new jersey']['middlesex county']['salesmen']=62
>>> a
defaultdict(<function __main__.<lambda>>,
        {'new jersey': defaultdict(<function __main__.<lambda>>,
                     {'mercer county': defaultdict(<function __main__.<lambda>>,
                                  {'plumbers': 3, 'programmers': 81}),
                      'middlesex county': defaultdict(<function __main__.<lambda>>,
                                  {'programmers': 81, 'salesmen': 62})})})

0
我可以将所有内容包装在一个类中,但似乎已经有人做过了。
开源ndicts软件包中的NestedDict类(我是作者)试图缓解处理嵌套字典的痛苦。我认为它满足了问题所要求的所有条件。
这里有它能力的摘要,更多详细信息请查看文档

初始化

>>> from ndicts import NestedDict
>>> nd = NestedDict({"a": {"aa": 0}, "b": 1})

获取项目

NestedDict视为已展开的字典。

>>> nd["a", "aa"]
0

同时,您可以获取中间节点,而不仅仅是叶子节点的值。
>>> nd["a"]
{"aa": 0}

如果键不存在,则抛出异常。
>>> nd["asd"]
Traceback (most recent call last):
...
KeyError: ('asd',)

设置项目

与普通字典一样,如果键缺失,则会将其添加到NestedDict中。

>>> nd["a", "ab"] = 2
>>> nd
NestedDict({"a": {"aa": 0, "ab": 2}, "b": 1})

这允许从一个空的NestedDict开始,可以通过设置新项来使其复活。

迭代

在迭代方面,将NestedDict视为一个扁平化的字典。熟悉的.keys().values().item()方法都可用。

>>> [key for key in nd]
[('a', 'aa'), ('a', 'ab'), ('b',)]
>>> [value for value in nd.values()]
[0, 2, 1]

0

我曾经使用过这个函数。它安全、快速、易于维护。

def deep_get(dictionary, keys, default=None):
    return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)

例子:

>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
...     return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>

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