什么是实现嵌套字典的最佳方式?

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

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

现在,维护和创build这个非常痛苦, 每当我有一个新的州/县/职业,我必须通过令人讨厌的try / catch块创build下层字典。 而且,如果我想查看所有的值,我必须创build恼人的嵌套迭代器。

我也可以使用元组作为键,就像这样:

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

这使得遍历这些值非常简单和自然,但是在聚合和查看字典的子集(例如,如果我只是想逐状态地进行)时,在语法上更加痛苦。

基本上,有时候我想把嵌套字典看成是一个扁平字典,有时候我想把它看成一个复杂的层次结构。 我可以把这一切都包装在一个class级,但似乎有人可能已经这样做了。 另外,似乎可能会有一些非常优雅的语法结构来做到这一点。

我怎么能做得更好?

附录:我知道setdefault()但它并不真正使语法清晰。 另外,您创build的每个子字典仍需要手动设置setdefault()

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

dict子类上实现__missing__来设置和返回一个新的实例!

下面是一个更加优雅的方法,自从Python2.5以来已经可用(和logging)了 ,并且对我来说(对我来说特别有价值) 它就像正常的字典一样打印 ,而不是打印一个autovivified默认值。

 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]在赋值的左边,所以在这里没有recursion。

这是2016年9月23日前所接受答案的一半代码行数。

说明:

我们只是提供了另一个我们的类Vividict嵌套实例,只要一个密钥被访问,但没有。 (返回赋值是有用的,因为它避免了我们另外调用字典上的getter,不幸的是,我们不能在设置它时返回它。)

请注意,这些语句与最上面的答案相同,但在代码行的一半 – nosklo的实现中:

 class AutoVivification(dict): """Implementation of perl's autovivification feature.""" def __getitem__(self, item): try: return dict.__getitem__(self, item) except KeyError: value = self[item] = type(self)() return value 

用法演示

下面只是一个例子,说明这个字典可以很容易地用来创build一个嵌套的字典结构。 这可以尽可能快地创build一个层次结构树结构。

 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__来为它的类设置一个新的实例并返回它是一个更好的解决scheme。

其他select,对比:

dict.setdefault

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需要一个新的实例,不pipe它是否被使用。 但是,Python在处理未使用和未被引用的新实例方面相当聪明,例如,它在内存中重用了该位置:

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

一个自动生成的defaultdict

这是一个干净的实现,在脚本中的使用,你没有检查数据将是实现__missing__

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

但是如果你需要检查你的数据,用相同方式填充数据的自动生成缺省值的结果如下所示:

 >>> 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>, {})})}) 

这个输出是相当不雅的,结果是相当难以理解的。 通常给出的解决scheme是recursion地转换回字典进行手动检查。 这个不重要的解决scheme是作为读者的练习。

性能

最后,我们来看看性能。 我正在减去实例化的成本。

 >>> 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笔记本中),那么性能并不重要 – 在这种情况下,我会使用Vividict来输出可读性。 与AutoVivification对象(使用__getitem__而不是__missing__ ,这是为此目的而devise)相比,它是非常优越的。

结论

在子类dict上实现__missing__来设置和返回一个新实例比替代方法稍微困难一点,但是具有

  • 容易实例化
  • 容易的数据人口
  • 方便数据查看

并且因为它比修改__getitem__更简单,性能也更好,所以应该优先select那个方法。

 class AutoVivification(dict): """Implementation of perl's autovivification feature.""" def __getitem__(self, item): try: return dict.__getitem__(self, item) except KeyError: value = self[item] = type(self)() return value 

testing:

 a = AutoVivification() a[1][2][3] = 4 a[1][3][3] = 5 a[1][2]['test'] = 6 print a 

输出:

 {1: {2: {'test': 6, 3: 4}, 3: {3: 5}}} 

只是因为我没有看到一个这么小的字,这是一个像你一样嵌套的字典,没有汗水:

 # yo dawg, i heard you liked dicts def yodict(): return defaultdict(yodict) 

你可以创build一个YAML文件并使用PyYaml读取它。

第1步:创build一个YAML文件“employment.yml”:

 new jersey: mercer county: pumbers: 3 programmers: 81 middlesex county: salesmen: 62 programmers: 81 new york: queens county: plumbers: 9 salesmen: 36 

第2步:在Python中阅读

 import yaml file_handle = open("employment.yml") my_shnazzy_dictionary = yaml.safe_load(file_handle) file_handle.close() 

现在my_shnazzy_dictionary有你所有的价值。 如果您需要这样做,您可以创buildYAML作为一个string,并将其馈送到yaml.safe_load(...)

既然你有一个星型模式devise,你可能想要更像一个关系表,而不像一个字典。

 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 

这种事情可以在创build类似于数据仓库的devise方面有很长的路要走,而不需要SQL开销。

如果嵌套层数很less,我使用collections.defaultdict来实现这个function:

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

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

 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 

你可能最终想要使新词不能添加到字典中。 recursion地将所有这些defaultdict转换成正常的dict是很容易的。

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

我发现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) 

正如其他人所build议的,关系数据库可能对您更有用。 您可以使用内存中的sqlite3数据库作为数据结构来创build表,然后查询它们。

 import sqlite3 c = sqlite3.Connection(':memory:') c.execute('CREATE TABLE jobs (state, county, title, count)') c.executemany('insert into jobs values (?, ?, ?, ?)', [ ('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), ]) # some example queries print list(c.execute('SELECT * FROM jobs WHERE county = "Queens County"')) print list(c.execute('SELECT SUM(count) FROM jobs WHERE title = "Programmers"')) 

这只是一个简单的例子。 您可以为州,县和职位定义单独的表格。

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) 

defaultdict()是你的朋友!

对于二维字典,你可以这样做:

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

对于更多维度,您可以:

 d = defaultdict(lambda :defaultdict(defaultdict)) d[1][2][3] = 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 

为了便于迭代你的嵌套字典,为什么不写一个简单的生成器?

 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块来读取树? 在尝试检索密钥之前查询密钥是否存在是很容易的(也可能更安全)。 使用guard子句的函数可能如下所示:

 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 ,它是Python2.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'] 

我在这里假设你的数据结构的含义,但是应该很容易调整你实际想要做的事情。

你可以使用Addict: https : //github.com/mewwts/addict

 >>> from addict import Dict >>> my_new_shiny_dict = Dict() >>> my_new_shiny_dict.abcde = 2 >>> my_new_shiny_dict {'a': {'b': {'c': {'d': {'e': 2}}}}} 

除非你的数据集保持相当小,否则你可能要考虑使用关系数据库。 它将完全按照你想要的方式进行:可以很容易地添加计数,select​​计数子集,甚至按州,县,职业或这些的任意组合来计算总计数。

 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 )时返回字典,否则单个值。

我喜欢把这个包装在一个类中,并实现__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

你可以在lambdas和defaultdict中使用recursion,不需要定义名字:

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

我有一个类似的事情。 我有很多的情况下,我做:

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

但是要深入很多层次。 这是“.get(item,{})”,这是关键,因为如果还没有一个字典,它会创build另一个字典。 同时,我一直在想办法更好地处理这个问题。 现在,有很多

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

更好? 我想是这样。

我曾经使用这个function。 其安全,快速,易于维护。

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