【问题标题】:Are there any reasons not to use an OrderedDict?有什么理由不使用 OrderedDict?
【发布时间】:2013-09-27 20:53:57
【问题描述】:

我指的是来自collections 模块的OrderedDict,它是一个有序字典。

如果它具有可订购的附加功能,我意识到这通常可能不是必需的,但即便如此,有什么缺点吗?是不是比较慢?它缺少任何功能吗?我没有看到任何缺失的方法。

简而言之,为什么不应该我总是使用这个而不是普通的字典?

【问题讨论】:

  • 此外,许多包返回 dicts 并且将它们与 OrderedDict 一起使用可能会弄乱顺序。
  • 我的问题是,为什么要使用 OrderedDict?为什么需要有序字典?
  • 我将 OrderedDict 仅用于输出格式。我还缺少其他用途吗?
  • @Haidro, an example 来自标准库。
  • 如果您使用 OrderedDict 的唯一目的是格式化输出(可能是排序键),只需使用 for key in sorted(dictvar): print (key, dictvar[key])。 OrderedDict 保留插入顺序,而不是键顺序。

标签: python dictionary python-3.x ordereddictionary


【解决方案1】:

OrderedDictdict 的子类,需要更多内存来跟踪添加键的顺序。这不是微不足道的。该实现在幕后添加了第二个dict,以及所有键的双向链接列表(这是记住顺序的部分),以及一堆弱引用代理。它不是很多慢,但至少比使用普通dict 的内存增加了一倍。

但如果合适,请使用它!这就是它存在的原因:-)

工作原理

基本 dict 只是一个普通的 dict 映射键到值 - 它根本不是“有序”的。添加 <key, value> 对时,key 将附加到列表中。列表是记住顺序的部分。

但如果这是一个 Python 列表,删除一个键将花费 O(n) 两次的时间:O(n) 时间在列表中找到键,O(n) 时间删除列表中的键。

所以它是一个双向链表。这使得删除键常量 (O(1)) 成为时间。但是我们仍然需要找到属于该键的双向链表节点。为了使O(1) 的操作也计时,第二个隐藏的字典将键映射到双向链表中的节点。

所以添加一个新的<key, value> 对需要将该对添加到基本字典中,创建一个新的双向链表节点来保存密钥,将该新节点附加到双向链表,并将密钥映射到那个隐藏字典中的新节点。工作量是原来的两倍多一点,但总体上仍然需要 O(1)(预期情况)时间。

同样,删除一个存在的键也需要两倍多的工作量,但O(1) 总体预期时间:使用隐藏的字典找到键的双向链表节点,从列表中删除该节点,然后删除两个字典的键。

等等。效率很高。

【讨论】:

  • @GrijeshChauhan,我阅读了源代码 - 我是一名核心 Python 开发人员,所以这就是我回答 遇到的大多数问题的方式 - 哈哈 ;-) 你可以找到Python 源代码树中Lib/collections/__init__.py 中的代码。
  • 等等...你是写 TIMSORT 的人!!!从蟒蛇天堂意外降临回答我卑微的问题。谢谢!
  • 哈哈!不客气,@Aerovistae - 这是一个有价值的问题 ;-)
  • 我发现当我告诉人们“你可以在 Python 源代码树中找到代码”时,他们从不看,但当我 link to the hg repo 时,他们有时会看。 (通常只有在阅读源代码时才会让他们想到一个让我头疼的问题。)
  • @GrijeshChauhan 进入你的python解释器,输入import this然后回车,写它的人就是回答这个问题的人。
【解决方案2】:

多线程

如果您的字典是在没有锁的情况下从多个线程访问的,尤其是作为同步点。

vanilla dict 操作是原子的,Python 中扩展的任何类型都不是。

事实上,我什至不确定 OrderedDict 是线程安全的(没有锁),尽管 我不能否认它被非常仔细地编码并满足重入定义的可能性。

小恶魔

创建大量此类词典时的内存使用情况

如果您的代码所做的只是处理这些字典,则 cpu 使用率

【讨论】:

    【解决方案3】:

    为什么我不应该总是使用它而不是普通字典

    在 Python 2.7 中,正常使用 OrderedDict 会创建引用循环。因此,任何使用OrderedDict 都需要启用垃圾收集器才能释放内存。是的,垃圾收集器在 cPython 中默认开启,但禁用它has its uses

    例如使用 cPython 2.7.14

    from __future__ import print_function
    
    import collections
    import gc
    
    if __name__ == '__main__':
        d = collections.OrderedDict([('key', 'val')])
        gc.collect()
        del d
        gc.set_debug(gc.DEBUG_LEAK)
        gc.collect()
        for i, obj in enumerate(gc.garbage):
            print(i, obj)
    

    输出

    gc: collectable <list 00000000033E7908>
    gc: collectable <list 000000000331EC88>
    0 [[[...], [...], 'key'], [[...], [...], 'key'], None]
    1 [[[...], [...], None], [[...], [...], None], 'key']
    

    即使您只是创建一个空的OrderedDict (d = collections.OrderedDict()) 并且不向其中添加任何内容,或者您​​明确地尝试通过调用clear 方法来清理它(d.clear()del d 之前),你仍然会得到一个自引用列表:

    gc: collectable <list 0000000003ABBA08>
    0 [[...], [...], None]
    

    自从this commit 删除了__del__ 方法以防止OrderedDict 可能导致无法收集的循环(可以说是更糟糕的循环)后,情况似乎就是如此。如该提交的更改日志中所述:

    Issue #9825:从 collections.OrderedDict 的定义中删除了 __del__。 这可以防止用户创建的自引用有序字典 成为永久无法收集的 GC 垃圾。缺点是 删除 __del__ 意味着内部双向链表必须等待 当 refcnt 下降时,GC 收集而不是立即释放内存 归零。


    请注意,在 Python 3 中,针对同一问题的 fix 采用不同的方式并使用弱引用代理来避免循环:

    问题 #9825:在 collections.OrderedDict 的定义中使用 __del__ 用户可以创建自引用的有序字典 成为永久无法收集的 GC 垃圾。恢复 Py3.1 使用弱引用代理的方法,这样引用循环就不会被创建 首先。

    【讨论】:

      【解决方案4】:

      从 Python 3.7 开始,所有字典都保证是有序的。 Python 贡献者确定切换到使dict 有序不会对性能产生负面影响。我不知道OrderedDict 的性能与 Python >= 3.7 中的dict 相比如何,但我想它们是可比的,因为它们都是有序的。

      请注意,OrderedDictdict 的行为仍然存在差异。另见:Will OrderedDict become redundant in Python 3.7?

      【讨论】:

        猜你喜欢
        • 2010-09-29
        • 2011-11-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-08
        • 1970-01-01
        • 2010-10-11
        • 2010-10-10
        相关资源
        最近更新 更多