我有一个数据结构,基本上相当于嵌套字典.让我们说它看起来像这样:
{'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()
手动设置.
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
测试:
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}}}
在Python中实现嵌套字典的最佳方法是什么?
__missing__
在dict
子类上实现以设置和返回新实例.
自Python 2.5以来,这种方法已经可用(并且有文档记录),并且(对我来说特别有价值)它可以像普通的dict一样打印,而不是自动生成的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'] {}
另外,现在我们的数据中有一个拼写错误的县:
>>> 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': {}}}说明:
Vividict
每当访问一个密钥但缺少密钥时,我们只是提供另一个类的嵌套实例.(返回值赋值很有用,因为它避免了我们在dict上另外调用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
下面只是一个示例,说明如何轻松地使用此dict来动态创建嵌套的dict结构.这可以快速创建一个您可能想要的分层树结构.
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
虽然提问者认为这不干净,但我发现它比Vividict
我自己更好.
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 "", line 1, in 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)
这是一个整洁的实现,并且在您不检查数据的脚本中使用将与实现一样有用__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(, {'foo': defaultdict( , {'baz': defaultdict( , {}), 'bar': defaultdict( , {})}), 'primary': defaultdict( , {'secondary': defaultdict( , {'tertiary': defaultdict( , {'quaternary': defaultdict( , {})})})}), 'fizz': defaultdict( , {'buzz': defaultdict( , {})})})
这个输出非常不优雅,结果非常难以理解.通常给出的解决方案是递归地转换回dict以进行手动检查.这个非平凡的解决方案留给读者练习.
性能最后,让我们来看看性能.我减去了实例化的成本.
>>> 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__
为此目的而使用)相比,它更优越.
实现__missing__
一个子类的dict
设置,并返回一个新的实例比替代稍有难度,但带来的利益
容易实例化
容易的数据填充
轻松查看数据
并且因为它比修改更简单和更高效__getitem__
,所以应该优先考虑该方法.
然而,它有缺点:
错误的查找将无声地失败.
错误的查找将保留在字典中.
因此,我个人更喜欢setdefault
其他解决方案,并且在我需要这种行为的每种情况下都有.
只是因为我没有看到这么小的一个,这里有一个像你喜欢的嵌套,没有汗水:
# yo dawg, i heard you liked dicts def yodict(): return defaultdict(yodict)
您可以创建一个YAML文件并使用PyYaml读取它.
第1步:创建一个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
拥有你所有的价值观.如果您需要动态执行此操作,则可以将YAML创建为字符串并将其输入yaml.safe_load(...)
.
由于您具有星型模式设计,因此您可能希望将其更像是关系表而不是字典.
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开销的情况下创建类似数据仓库的设计.
如果嵌套级别的数量很小,我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()
等等.
这是一个返回任意深度的嵌套字典的函数:
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
您最终可能希望将其设置为无法将新项目添加到dict中.递归地将所有这些转换defaultdict
为正常dict
s 很容易.
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)
正如其他人所建议的那样,关系数据库可能对您更有用.您可以使用内存中的sqlite3数据库作为数据结构来创建表,然后查询它们.
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