我有一个数据结构,本质上相当于一个嵌套的字典。假设它是这样的:

{'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()。


当前回答

由于您有一个星型模式设计,您可能希望它的结构更像一个关系表,而不是字典。

import collections

class Jobs( object ):
    def __init__( self, state, county, title, count ):
        self.state= state
        self.count= county
        self.title= title
        self.count= count

facts = [
    Jobs( 'new jersey', 'mercer county', 'plumbers', 3 ),
    ...

def groupBy( facts, name ):
    total= collections.defaultdict( int )
    for f in facts:
        key= getattr( f, name )
        total[key] += f.count

这类事情对于创建一个没有SQL开销的类似数据仓库的设计大有帮助。

其他回答

我发现setdefault非常有用;它检查一个键是否存在,如果不存在就添加它:

d = {}
d.setdefault('new jersey', {}).setdefault('mercer county', {})['plumbers'] = 3

Setdefault总是返回相关的键,所以你实际上是在原地更新'd'的值。

说到迭代,我相信你可以很容易地编写一个生成器,如果Python中还没有这样的生成器:

def iterateStates(d):
    # Let's count up the total number of "plumbers" / "dentists" / etc.
    # across all counties and states
    job_totals = {}

    # I guess this is the annoying nested stuff you were talking about?
    for (state, counties) in d.iteritems():
        for (county, jobs) in counties.iteritems():
            for (job, num) in jobs.iteritems():
                # If job isn't already in job_totals, default it to zero
                job_totals[job] = job_totals.get(job, 0) + num

    # Now return an iterator of (job, number) tuples
    return job_totals.iteritems()

# Display all jobs
for (job, num) in iterateStates(d):
    print "There are %d %s in total" % (job, num)

我可以把这些都打包到一个类中,但似乎有人已经这样做了。

来自开源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]

可以对Collections.defaultdict进行子类化以生成嵌套的dict。然后向该类添加任何有用的迭代方法。

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

你可以在lambdas和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})})})

至于“讨厌的尝试/捕捉障碍”:

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