【问题标题】:Efficiently accessing arbitrarily deep dictionaries高效访问任意深度的字典
【发布时间】:2018-10-01 10:16:43
【问题描述】:

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

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 范围的方法。

谢谢

【问题讨论】:

  • 您的所有中间字典都是长度为 1 的吗?如果是,您可以相当有效地使用元组键。
  • 这会为我抛出KeyError: 'second'
  • @theausome - 答案“......似乎不适用于嵌套字典。”
  • 如果你想提高性能,你必须做出一些取舍。什么更可能更频繁地更改 - 您正在遍历的字典或您用来遍历的点符号字符串?如果两者都经常变化并且具有相同的重要性,那么您不会比@tdelaney 解决方案中提供的更快。

标签: python python-2.7 dictionary recursion


【解决方案1】:

实际上只有一种解决方案。重建你的字典。但只做一次。

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,如果您要在 many 类似的数据结构上执行此操作一次,那么仅针对单个查询进行预处理是没有意义的,在这种情况下,您可能更喜欢其他解决方案之一。

【讨论】:

  • 请注意,这似乎只允许访问嵌套的最终级别,例如您将无法访问new_dict['first.second']
  • @chrisz 如果需要,可以通过缓存res = recursive_flatten(v)、使用d.update(res) 更新d 以及然后 以类似方式迭代res 来解决此问题.
  • 直接使用dict确实是唯一快速的解决方案。
  • 虽然就空间而言,您的(在 cmets 中扩展)解决方案不能很好地扩展(线性读取)。
  • 我相信这可能是一个很好的欺骗目标,但既然你放了赏金,我想问一下? stackoverflow.com/questions/14692690/…
【解决方案2】:

我更新了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'

关于性能的说明:与标准字典访问相比,此答案速度较慢,我只是想提供一个实际使用“点访问”字典的选项。

【讨论】:

    【解决方案3】:

    通过稍微收紧代码,我获得了 20% 的性能提升,但通过使用缓存来拆分字符串,性能提升了惊人的 400%。如果您多次使用相同的规范,那只会有所不同。以下是示例实现和要测试的配置文件脚本。

    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
    

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

    【讨论】:

    • 看来你是唯一一个关注性能的人。
    • @kabanus 我不明白你的意思。只要您对数据进行一次预处理,您就可以使用我的解决方案获得纳秒级的性能。能不能做到这一点是OP,而不是我。
    • @COLDSPEED 我认为您和我之间的选择是在一个数据集上完成大量查询还是在多个数据集上完成一些查询。
    • 是的,有一些权衡:)
    • @cᴏʟᴅsᴘᴇᴇᴅ 是的 :) 我对你有偏见,因为这看起来像是在作弊,但回头看,我想我只是嫉妒。
    【解决方案4】:

    这是一个类似于 chrisz 的解决方案,但您不必事先对您的 dict 进行任何操作。 :

    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
    

    所以客观化字典的速度是常规字典查找的 3 倍 - 所以如果速度很重要,为什么要这样做?

    【讨论】:

    • No answer here has actually关注性能,包括您声称的答案。如果要进行数百万次访问,这些解决方案都没有任何好处 - 所有这些都加起来。
    • @cᴏʟᴅsᴘᴇᴇᴅ 嘿,至少给我“努力”的考虑。我正在尝试实际上需要.a.b.c.d 才能更深入地进入迷宫的东西。
    • 好的,我给你一个“不错的努力”考虑 (+1)。我确实喜欢你的答案,它和所有其他答案一样,肯定比我的优点。
    【解决方案5】:

    我也有同样的需求,所以我创建了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
    

    如你所见,它的性能介于“dict dotter”和“dot dict”之间。 我们将不胜感激任何性能增强建议。

    【讨论】:

      【解决方案6】:

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

      数据

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

      即使是动态代码执行,执行时间也会更短!!

      【讨论】:

      • 我看不出这与他们不想要的 OP 解决方案有何不同。
      • 如您所见,没有使用额外的变量来存储值,从而节省了执行时间,并且时间差以微秒为单位,因此当此代码执行一百万次时,这将是有效的另一个代码。此外,您可以使用 first, first.second , first.second.third 作为 arg 而无需更改单行代码。
      • 额外的变量使差异接近于 0,我当然希望在一百万条记录上获得比这更大的性能提升。
      • @cᴏʟᴅsᴘᴇᴇᴅ 你能告诉我如果你真的测量这段代码需要多少时间!!因为我敢肯定,这段代码在使用额外变量和没有额外变量的情况下执行的时间差异很大。
      • 几乎没有其他答案那么多,我们会继续这样做。
      【解决方案7】:

      你可以使用reduce(python3中的functools.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
      

      使用减少:

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

      【讨论】:

        猜你喜欢
        • 2019-05-28
        • 1970-01-01
        • 2016-01-30
        • 2015-08-19
        • 1970-01-01
        • 2013-04-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多