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

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


当前回答

如果嵌套层的数量很少,我使用collections.defaultdict:

from collections import defaultdict

def nested_dict_factory(): 
  return defaultdict(int)
def nested_dict_factory2(): 
  return defaultdict(nested_dict_factory)
db = defaultdict(nested_dict_factory2)

db['new jersey']['mercer county']['plumbers'] = 3
db['new jersey']['mercer county']['programmers'] = 81

像这样使用defaultdict可以避免大量混乱的setdefault()、get()等。

其他回答

在Python中实现嵌套字典的最佳方法是什么?

这是个坏主意,别这么做。相反,使用普通的字典和字典。因此,当正常使用下缺少键时,您将获得预期的KeyError。如果你坚持这种行为,下面是搬起石头砸自己的脚的方法:

在dict子类上实现__missing__来设置并返回一个新实例。

这种方法自Python 2.5以来就已经可用(并有文档),并且(对我来说特别有价值)它像普通的dict一样漂亮地打印,而不是像autovivified defaultdict那样难看地打印:

class Vividict(dict):
    def __missing__(self, key):
        value = self[key] = type(self)() # retain local pointer to value
        return value                     # faster to return than dict lookup

(注意self[key]在赋值的左边,所以这里没有递归。)

假设你有一些数据:

data = {('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}

下面是我们的用法代码:

vividict = Vividict()
for (state, county, occupation), number in data.items():
    vividict[state][county][occupation] = number

现在:

>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

批评

对这种类型的容器的批评是,如果用户拼写错了一个键,我们的代码可能会无声地失败:

>>> vividict['new york']['queens counyt']
{}

另外,现在我们的数据中有一个拼错的county:

>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36},
              'queens counyt': {}}}

解释:

我们只是提供了类的另一个嵌套实例,每当一个键被访问但缺失时。(返回赋值是有用的,因为它避免了我们额外调用dict上的getter,不幸的是,我们不能在它被设置时返回它。)

注意,这些和被点赞最多的答案是相同的语义,但是只有一半的代码行——nosklo的实现:

类AutoVivification (dict类型): "" perl的自动激活功能的实现。""" Def __getitem__(self, item): 试一试: 返回dict类型。__getitem__(自我,项) 除了KeyError: Value = self[item] = type(self)() 返回值

使用说明

下面是一个示例,说明如何轻松地使用这个字典创建一个嵌套的字典结构。这可以快速创建您想要深入的层次树结构。

import pprint

class Vividict(dict):
    def __missing__(self, key):
        value = self[key] = type(self)()
        return value

d = Vividict()

d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)

输出:

{'fizz': {'buzz': {}},
 'foo': {'bar': {}, 'baz': {}},
 'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}

正如最后一行所示,它打印得非常漂亮,便于人工检查。但如果你想要可视化地检查你的数据,实现__missing__来设置它的类的一个新实例为键并返回它是一个更好的解决方案。

作为对比,其他选择:

dict.setdefault

尽管提问者认为这个不干净,但我觉得它比我自己的livelict更可取。

d = {} # or dict()
for (state, county, occupation), number in data.items():
    d.setdefault(state, {}).setdefault(county, {})[occupation] = number

现在:

>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
                                  'programmers': 81},
                'middlesex county': {'programmers': 81,
                                     'salesmen': 62}},
 'new york': {'queens county': {'plumbers': 9,
                                'salesmen': 36}}}

一个拼写错误会引起混乱,也不会让我们的数据充满错误信息:

>>> d['new york']['queens counyt']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'

此外,我认为setdefault在循环中使用时工作得很好,你不知道你将得到什么键,但重复使用变得相当繁重,我不认为任何人会想要保持以下:

d = dict()

d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})

另一个批评是,无论是否使用setdefault,它都需要一个新实例。然而,Python(或者至少是CPython)在处理未使用和未引用的新实例方面相当聪明,例如,它重用内存中的位置:

>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)

一个自动激活的defaultdict

这是一个整洁的实现,在你没有检查数据的脚本中使用与实现__missing__一样有用:

from collections import defaultdict

def vivdict():
    return defaultdict(vivdict)

但是如果你需要检查你的数据,一个自动激活的defaultdict以同样的方式填充数据的结果看起来像这样:

>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint; 
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict 
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar': 
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function 
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>, 
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at 
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})

这个输出非常不美观,结果也非常难以阅读。通常给出的解决方案是递归地转换回字典以供人工检查。这个非平凡的解留给读者作为练习。

性能

最后,让我们看看性能。减去实例化的代价。

>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747

基于性能,dict。Setdefault的效果最好。在关心执行速度的情况下,我强烈推荐它用于生产代码。

如果你需要它来进行交互使用(也许在IPython笔记本中),那么性能并不重要——在这种情况下,为了输出的可读性,我会选择vivid。与AutoVivification对象(它使用__getitem__而不是__missing__,后者是为此目的而创建的)相比,它要优越得多。

结论

在子类dict上实现__missing__来设置并返回一个新实例比其他方法稍微困难一些,但具有的优点

简单的实例化 简单的数据填充 轻松查看数据

因为它比修改__getitem__更简单,性能更好,所以应该优先使用该方法。

然而,它也有缺点:

错误的查找将会无声地失败。 错误的查找将保留在字典中。

因此,与其他解决方案相比,我个人更喜欢使用setdefault,并且在需要这种行为的每种情况下都使用它。

为了方便遍历嵌套的字典,为什么不写一个简单的生成器呢?

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']

我在这里对数据结构的含义做了假设,但是根据实际需要进行调整应该很容易。

我也有类似的事情。我有很多这样的案例:

thedict = {}
for item in ('foo', 'bar', 'baz'):
  mydict = thedict.get(item, {})
  mydict = get_value_for(item)
  thedict[item] = mydict

但要深入很多层次。这是“。”Get (item,{})",这是一个键,因为如果已经没有字典,它将创建另一个字典。与此同时,我一直在想办法对付 这个更好。现在,有很多

value = mydict.get('foo', {}).get('bar', {}).get('baz', 0)

所以,我做了:

def dictgetter(thedict, default, *args):
  totalargs = len(args)
  for i,arg in enumerate(args):
    if i+1 == totalargs:
      thedict = thedict.get(arg, default)
    else:
      thedict = thedict.get(arg, {})
  return thedict

如果你这样做,效果是一样的:

value = dictgetter(mydict, 0, 'foo', 'bar', 'baz')

更好吗?我想是的。

对于下面(从上面复制),是否有一种方法来实现append函数。我试图使用嵌套字典存储值作为数组。

class Vividict(dict):
    def __missing__(self, key):
        value = self[key] = type(self)() # retain local pointer to value
    return value  

我目前的实现如下:

totalGeneHash=Vividict()
        
for keys in GenHash:
    for second in GenHash[keys]:
        if keys in sampleHash:
            total_val = GenHash[keys][second]
                totalGeneHash[gene][keys].append(total_val)
This is the error I get: AttributeError: 'Vividict' object has no attribute 'append'

这是一个返回任意深度的嵌套字典的函数:

from collections import defaultdict
def make_dict():
    return defaultdict(make_dict)

像这样使用它:

d=defaultdict(make_dict)
d["food"]["meat"]="beef"
d["food"]["veggie"]="corn"
d["food"]["sweets"]="ice cream"
d["animal"]["pet"]["dog"]="collie"
d["animal"]["pet"]["cat"]="tabby"
d["animal"]["farm animal"]="chicken"

迭代所有内容,如下所示:

def iter_all(d,depth=1):
    for k,v in d.iteritems():
        print "-"*depth,k
        if type(v) is defaultdict:
            iter_all(v,depth+1)
        else:
            print "-"*(depth+1),v

iter_all(d)

打印出来:

- food
-- sweets
--- ice cream
-- meat
--- beef
-- veggie
--- corn
- animal
-- pet
--- dog
---- labrador
--- cat
---- tabby
-- farm animal
--- chicken

你可能最终想要这样做,使新的项目不能添加到字典。递归地将所有这些defaultdict转换为普通dict很容易。

def dictify(d):
    for k,v in d.iteritems():
        if isinstance(v,defaultdict):
            d[k] = dictify(v)
    return dict(d)