有效地访问任意深度的词典

时间:2018-04-20 16:26:10

标签: python python-2.7 dictionary recursion

假设我有一个像这样的多级字典

mydict = {
    'first': {
        'second': {
            'third': {
                'fourth': 'the end'
             }
         }
     }
}

我想像这样访问它

test = get_entry(mydict, 'first.second.third.fourth')

到目前为止我所拥有的是

def get_entry(dict, keyspec):
    keys = keyspec.split('.')

    result = dict[keys[0]]
    for key in keys[1:]:
       result = dict[key]

    return result

有更有效的方法吗?根据%timeit,函数的运行时间是1.26us,而像这样的标准方式访问字典

foo = mydict['first']['second']['third']['fourth']

需要541ns。如果可能的话,我正在寻找将其修剪到800ns的方法。

由于

7 个答案:

答案 0 :(得分:12)

真的只有一个解决方案。重建你的字典。但只做一次。

def recursive_flatten(mydict):
    d = {}
    for k, v in mydict.items():
        if isinstance(v, dict):
            for k2, v2 in recursive_flatten(v).items():
                d[k + '.' + k2] = v2 
        else:
            d[k] = v
    return d

In [786]: new_dict = recursive_flatten(mydict); new_dict
Out[786]: {'first.second.third.fourth': 'the end'}

(还有一些测试)

In [788]: recursive_flatten({'x' : {'y' : 1, 'z' : 2}, 'y' : {'a' : 5}, 'z' : 2})
Out[788]: {'x.y': 1, 'x.z': 2, 'y.a': 5, 'z': 2}

In [789]: recursive_flatten({'x' : 1, 'y' : {'x' : 234}})
Out[789]: {'x': 1, 'y.x': 234}

从此处开始,每次访问都会成为固定时间。

现在,只需使用new_dict['first.second.third.fourth']访问您的值即可。应该适用于任何包含自引用的任意嵌套字典。

请注意,每个解决方案都有其公平的权衡,这也不例外。除非您在数据上发出数百万条查询,以便预处理是可接受的开销,否则就是这样。使用其他解决方案,您只是回避问题而不是解决问题 - 这是处理字典的结构。 OTOH,如果您要在许多这样的类似数据结构上一次 ,那么仅针对单个查询进行预处理是没有意义的,在这种情况下,可能更喜欢其他解决方案之一。

答案 1 :(得分:11)

通过将代码收紧一点,但通过使用缓存来分割字符串,我的性能提升了20%,性能提升了20%。如果您多次使用相同的规格,那只会产生影响。以下是示例实现和要测试的配置文件脚本。

test.py

mydict = {
    'first': {
        'second': {
            'third': {
                'fourth': 'the end'
             }
         }
     }
}

# original
def get_entry(dict, keyspec):
    keys = keyspec.split('.')

    result = dict[keys[0]]
    for key in keys[1:]:
       result = result[key]

    return result

# tighten up code
def get_entry_2(mydict, keyspec):
    for key in keyspec.split('.'):
        mydict = mydict[key]
    return mydict

# use a cache
cache = {}
def get_entry_3(mydict, keyspec):
    global cache
    try:
        spec = cache[keyspec]
    except KeyError:
        spec = tuple(keyspec.split('.'))
        cache[keyspec] = spec

    for key in spec:
        mydict = mydict[key]
    return mydict

if __name__ == "__main__":
    test = get_entry(mydict, 'first.second.third.fourth')
    print(test)

profile.py

from timeit import timeit
print("original get_entry")
print(timeit("get_entry(mydict, 'first.second.third.fourth')",
    setup="from test import get_entry, mydict"))

print("get_entry_2 with tighter code")
print(timeit("get_entry_2(mydict, 'first.second.third.fourth')",
    setup="from test import get_entry_2, mydict"))

print("get_entry_3 with cache of split spec")
print(timeit("get_entry_3(mydict, 'first.second.third.fourth')",
    setup="from test import get_entry_3, mydict"))

print("just splitting a spec")
print(timeit("x.split('.')", setup="x='first.second.third.fourth'"))

我的机器上的时间是

original get_entry
4.148535753000033
get_entry_2 with tighter code
3.2986323120003362
get_entry_3 with cache of split spec
1.3073233439990872
just splitting a spec
1.0949148639992927

请注意,拆分规范对于此功能来说是一项相对昂贵的操作。这就是缓存有用的原因。

答案 2 :(得分:10)

我更新了How to use a dot "." to access members of dictionary?的答案,使用初始转换,然后对嵌套词典起作用:

您可以使用以下类来允许字典的点索引:

class dotdict(dict):
    """dot.notation access to dictionary attributes"""
    __getattr__ = dict.get
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

但是,如果所有嵌套字典 类型为dotdict,则仅支持嵌套。这是以下辅助函数的来源:

def dct_to_dotdct(d):
    if isinstance(d, dict):
        d = dotdict({k: dct_to_dotdct(v) for k, v in d.items()})
    return d

此函数必须在嵌套字典上运行一次,然后可以使用点索引对结果进行索引。

以下是一些例子:

In [13]: mydict
Out[13]: {'first': {'second': {'third': {'fourth': 'the end'}}}}

In [14]: mydict = dct_to_dotdct(mydict)

In [15]: mydict.first.second
Out[15]: {'third': {'fourth': 'the end'}}

In [16]: mydict.first.second.third.fourth
Out[16]: 'the end'

关于性能的说明:与标准字典访问相比,这个答案很慢,我只想提供一个实际使用的选项" dot access"到字典。

答案 3 :(得分:7)

这是一个类似于chrisz的解决方案,但你不需要事先做任何事情。 :

class dictDotter(dict):
    def __getattr__(self,key):
        val = self[key]
        return val if type(val) != dict else dictDotter(val)

只有x=dictDotter(originalDict)会让你获得任意点(`x.first.second ...)。我注意到它的速度是chrisz解决方案的两倍,而且速度是你的速度的9倍(在我的机器上,大约是)。

所以,如果你坚持做这项工作,@ tdelaney似乎提供了唯一真正的性能提升。

另一种选择比你拥有的更好(就运行时而言):

class dictObjecter:
    def __init__(self,adict):
        for k,v in adict.items():
            self.__dict__[k] = v
            if type(v) == dict: self.__dict__[k] = dictObjecter(v)

这将使你的dict中出现一个对象,所以点符号通常是正常的。这样可以将运行时间提高到你所拥有的<3> ,这也不错,但代价是要超越你的dict,并用其他东西替换它。

以下是总测试代码:

from timeit import timeit

class dictObjecter:
    def __init__(self,adict):
        for k,v in adict.items():
            self.__dict__[k] = v
            if type(v) == dict: self.__dict__[k] = dictObjecter(v)

class dictDotter(dict):
    def __getattr__(self,key):
        val = self[key]
        return val if type(val) != dict else dictDotter(val)

def get_entry(dict, keyspec):
    keys = keyspec.split('.')

    result = dict[keys[0]]
    for key in keys[1:]:
        result = result[key]

    return result

class dotdict(dict):
    """dot.notation access to dictionary attributes"""
    __getattr__ = dict.get
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

def dct_to_dotdct(d):
    if isinstance(d, dict):
        d = dotdict({k: dct_to_dotdct(v) for k, v in d.items()})
    return d

x = {'a':{'b':{'c':{'d':1}}}}
y = dictDotter(x)
z = dct_to_dotdct(x)
w = dictObjecter(x)
print('{:15} : {}'.format('dict dotter',timeit('y.a.b.c.d',globals=locals(),number=1000)))
print('{:15} : {}'.format('dot dict',timeit('z.a.b.c.d',globals=locals(),number=1000)))
print('{:15} : {}'.format('dict objecter',timeit('w.a.b.c.d',globals=locals(),number=1000)))
print('{:15} : {}'.format('original',timeit("get_entry(x,'a.b.c.d')",globals=locals(),number=1000)))
print('{:15} : {:.20f}'.format('best ref',timeit("x['a']['b']['c']['d']",globals=locals(),number=1000)))

我提供了最后一次常规查找作为最佳参考.Windows Ubuntu子系统上的结果:

dict dotter     : 0.0035500000003594323
dot dict        : 0.0017939999997906853
dict objecter   : 0.00021699999979318818
original        : 0.0006629999998040148
best ref        : 0.00007999999979801942

因此,物化dict的速度是常规字典查找速度的3倍 - 所以如果速度很重要,为什么要这样呢?

答案 4 :(得分:2)

我有同样的需求,所以我创建了Prodict

对于您的情况,您可以在一行中完成:

mydict = {
    'first': {
        'second': {
            'third': {
                'fourth': 'the end'
             }
         }
     }
}
dotdict = Prodict.from_dict(mydict)
print(dotdict.first.second.third.fourth) # "the end"

之后,像dict一样使用dotdict,因为它是dict的子类:

dotdict.first == dotdict['first'] # True

您还可以使用点表示法动态添加更多键:

dotdict.new_key = 'hooray'
print(dotdict.new_key) # "hooray"

即使新密钥是嵌套字典,它仍然有效:

dotdict.it = {'just': 'works'}
print(dotdict.it.just)  # "works"

最后,如果您事先定义了密钥,则会获得自动完成和自动类型转换:

class User(Prodict):
    user_id: int
    name: str

user = User(user_id="1", "name":"Ramazan")
type(user.user_id) # <class 'int'>
# IDE will be able to auto complete 'user_id' and 'name' properties

<强>更新

这是@kabanus编写的相同代码的测试结果:

x = {'a': {'b': {'c': {'d': 1}}}}
y = dictDotter(x)
z = dct_to_dotdct(x)
w = dictObjecter(x)
p = Prodict.from_dict(x)

print('{:15} : {}'.format('dict dotter', timeit('y.a.b.c.d', globals=locals(), number=10000)))
print('{:15} : {}'.format('prodict', timeit('p.a.b.c.d', globals=locals(), number=10000)))
print('{:15} : {}'.format('dot dict', timeit('z.a.b.c.d', globals=locals(), number=10000)))
print('{:15} : {}'.format('dict objecter', timeit('w.a.b.c.d', globals=locals(), number=10000)))
print('{:15} : {}'.format('original', timeit("get_entry(x,'a.b.c.d')", globals=locals(), number=10000)))
print('{:15} : {:.20f}'.format('prodict getitem', timeit("p['a']['b']['c']['d']", globals=locals(), number=10000)))
print('{:15} : {:.20f}'.format('best ref', timeit("x['a']['b']['c']['d']", globals=locals(), number=10000)))

结果:

dict dotter     : 0.04535976458466595
prodict         : 0.02860781018446784
dot dict        : 0.019078164088831673
dict objecter   : 0.0017378700050722368
original        : 0.006594238310349346
prodict getitem : 0.00510931794975705289
best ref        : 0.00121740293554022105

正如您所看到的,它的表现介于&#34; dict dotter&#34;和&#34; dot dict&#34;。 任何性能增强建议将不胜感激。

答案 5 :(得分:1)

代码应该更少迭代,更具动态性!

数据

mydict = {
    'first': {
        'second': {
            'third': {
                'fourth': 'the end'
             }
         }
     }
}

功能

def get_entry(dict, keyspec):
    for keys in keyspec.split('.'):
        dict = dict[keys]
    return dict

调用函数

res = get_entry(mydict, 'first.second.third.fourth')

这将花费更少的时间来执行,即使它是动态代码执行!!

答案 6 :(得分:1)

您可以在{python3中使用reducefunctools.reduce):

import operator
def get_entry(dct, keyspec):
    return reduce(operator.getitem, keyspec.split('.'), dct)

看起来更漂亮,但性能稍差。

您的版本时间:

>>> timeit("get_entry_original(mydict, 'first.second.third.fourth')",
           "from __main__ import get_entry_original, mydict", number=1000000)
0.5646841526031494

with reduce:

>>> timeit("get_entry(mydict, 'first.second.third.fourth')",
           "from __main__ import get_entry, mydict")
0.6140949726104736

作为tdelaney通知 - 分裂消耗的cpu功率几乎与获取dict中的密钥一样多:

def split_keys(keyspec):
    keys = keyspec.split('.')

timeit("split_keys('first.second.third.fourth')",
       "from __main__ import split_keys")
0.28857898712158203

只需将字符串拆分远离get_entry功能:

def get_entry(dct, keyspec_list):
    return reduce(operator.getitem, keyspec_list, dct)


timeit("get_entry(mydict, ['first', 'second', 'third', 'fourth'])",
       "from __main__ import get_entry, mydict")
0.37825703620910645