【问题标题】:Python ordered garbage collectible dictionary?Python订购了垃圾收集字典?
【发布时间】:2012-09-21 22:26:39
【问题描述】:

我希望我的 Python 程序具有确定性,因此我一直在整个代码中广泛使用 OrderedDicts。不幸的是,今天在调试内存泄漏时,我发现 OrderedDicts 有一个自定义的__del__ 方法,只要有循环就无法收集它们。不幸的是,文档中没有关于此的警告。

那我该怎么办? Python 标准库中是否有任何与 gc 配合得很好的确定性字典?我真的不想自己动手,尤其是像这样愚蠢的单行函数。

另外,我应该为此提交错误报告吗?我不熟悉 Python 库的过程,以及他们认为的错误。

编辑:It appears that this is a known bug that was fixed back in 2010. I must have somehow gotten a really old version of 2.7 installed. 我想最好的方法是只包含一个猴子补丁,以防用户碰巧运行像我这样的损坏版本。

【问题讨论】:

  • 首先,您使用的是什么版本的 Python?看看 CPython 2.7,collections.OrderedDict 是纯 Python,它根本没有自定义 del 方法。
  • 另外,你到底想在这里完成什么?标准的dict 实际上是确定性的——一遍又一遍地运行同一个程序,并且顺序总是相同的。它不是可预测的(没有大量内部知识,或者只运行一次就可以看到你得到了什么),但是为什么你需要顺序是可预测的?
  • 即使这是你需要的,你也不需要自己动手; OrderedDict 的文档(在 2.7 和 3.3 中)具有指向“在 Python 2.4 或更高版本上运行的等效 OrderedDict 配方”的链接,并且没有 del;你有什么理由不能使用它吗?
  • 最后,Python 可以使用自定义 __del__ 方法打破对象的循环,它只需要帮助——你必须查看 gc.garbage 并为它打破循环(然后将其从 @987654329 中删除@, 当然)。这通常是您不想做的事情,但是如果您正在编写循环并且取决于 gc,您将不得不在某个时候学习。 (或者,当然,您可以停止编写周期,例如,使用 weakrefs。)
  • @abarnert 我正在运行 CPython 2.7,我的版本确实有一个自定义 __del__。此外,进行手动垃圾收集或重写所有内容以避免循环是一项巨大的工作。到那时,只需要修改 OrderedDict 就更容易了!

标签: python garbage-collection ordereddictionary


【解决方案1】:

如果 __del__ 方法的存在对您来说有问题,只需将其删除:

>>> import collections
>>> del collections.OrderedDict.__del__

您将获得在参考周期中使用 OrderedDicts 的能力。您将失去让 OrderedDict 在删除后立即释放其所有资源。

【讨论】:

  • 这就是我最终所做的。起初我犹豫是否要使用 Monkey Patching,因为它会导致所有潜在的危害,但当我发现这是一个已在更高版本中修复的错误时,我认为 Monkey Patching 是完全合理的。
  • 另外,它的资源被立即释放是一个红鲱鱼。如果它作为循环的一部分被释放,这意味着它所引用的任何无法访问的对象也将被释放。
  • @Antimony 另一方面,如果 OD 不是循环的一部分,则 __del__ 确实会在立即释放和必须等待 GC 清理底层双向链接之间产生影响列表。
  • 哦,它解释了它。我想知道首先添加该方法的基本原理是什么。
【解决方案2】:

听起来您在OrderedDict 中发现了一个错误,该错误在您的 2.7 版本之后的某个时间点得到了修复。如果它不在任何实际发布的版本中,也许你可以忽略它。但除此之外,是的,您需要一种解决方法。

我建议,您应该使用the documentation 中链接的Equivalent OrderedDict recipe that runs on Python 2.4 or later 代替collections.OrderedDict(没有多余的__del__),而不是monkeypatching collections.OrderedDict。如果不出意外,当有人走过来说“我需要在 2.6 上运行它,要移植多少工作”时,答案将是“少一点”……

还有两点:

重写所有内容以避免循环是一项巨大的工作。

您的字典中有循环这一事实是一个危险信号,表明您做错了(通常为缓存或反向指针使用强引用),这可能会导致其他内存问题,可能还有其他错误。所以无论如何,这种努力可能是必要的。

你还没有解释你想要完成什么;我怀疑“确定性”的东西只是一个红鲱鱼(特别是因为dicts 实际上是确定性的),所以最好的解决方案是s/OrderedDict/dict/g。

但是如果确定性是必要的,你就不能依赖循环收集器,因为它不是确定性的,这意味着你的终结器排序等等都变得不确定。这也意味着你的内存使用是不确定的——你最终可能会得到一个程序,它在 99.999% 的时间里,但不是 100% 都在你想要的内存范围内;如果这些界限非常重要,那可能比每次都失败更糟糕。

同时,没有指定字典的迭代顺序,但在实践中,CPython 和 PyPy 按照哈希桶的顺序进行迭代,而不是值或键的 id(内存位置),以及任何 Jython 和IronPython 确实(他们可能使用了一些具有不同行为的底层 Java 或 .NET 集合;我没有测试过),键的内存顺序不太可能相关。 (你怎么能有效地基于类似的东西迭代一个哈希表?)你可能已经对使用id 和hash 的对象进行测试感到困惑,但大多数对象都是基于值的哈希值。

以这个简单的程序为例:

d={}
d[0] = 0
d[1] = 1
d[2] = 2
for k in d:
  print(k, d[k], id(k), id(d[k]), hash(k))

如果您使用 CPython 2.7、CPython 3.2 和 PyPy 1.9 重复运行它,键将始终按 0、1、2 的顺序迭代。id 列可能也相同每次(取决于您的平台),但您可以通过多种方式修复它 - 以不同的顺序插入,反转值的顺序,使用字符串值而不是整数,将值分配给变量,然后插入那些变量而不是文字等。充分利用它,您可以获得id 列的所有可能顺序,但键仍然每次都以相同的顺序迭代。

迭代的顺序不是可预测的,因为要预测它,您需要将hash(k) 转换为存储桶索引的函数,这取决于您无法从 Python 访问的信息。即使只是hash(k) % self._table_size,除非将_table_size 暴露给Python 接口,否则也无济于事。 (这是一个插入和删除序列的复杂函数,原则上可以计算出来,但实际上尝试起来很愚蠢。)

但它是确定性的;如果每次都以相同的顺序插入和删除相同的key,那么每次的迭代顺序都是一样的。

【讨论】:

  • GC 对确定性来说不是问题,因为如果你没有终结器(现在有人真的使用这些终结器吗?),它没有副作用。你能解释一下你所说的dicts是确定性的吗?我的印象是迭代顺序是由对象的内存地址决定的,这些地址在每次运行中都存在不可预测的差异。
  • 我已经编辑了上面的答案,但很简短:即使您只依靠终结器来进行隐式发布,如果这些发布是不确定的,那么您的峰值内存使用就会变得不确定。
  • “大多数对象”确实使用 id 作为哈希,除非它们碰巧另有定义。
  • 但是大多数对象是另外定义的:int,str,bytes,unicode,tuple,frozenset,大多数不可变类在标准库等按值散列;大多数可变类型是不可散列的。事实上,所有类型都必须是其中一种,除非您还通过身份而不是值来比较它们(如type 或module)。无论如何,all 对象按哈希桶排序;在某些情况下碰巧涉及id,但这并不意味着所有对象都按id 排序。
  • 我还应该提一下:Python 不会阻止你对你定义的值类型使用 id 散列,但是你打破了 a == b 的不变量暗示hash(a) == hash(b),因此字典将无法正常工作。例如:如果foo 是id-hashed,d={foo(3): 3}; d[foo(3)] 可能会引发KeyError。
【解决方案3】:

请注意,the fix made in Python 2.7 消除了 __del__ 方法并阻止它们无法收集,这不幸地意味着每次使用 OrderedDict(即使是空的)都会导致引用循环,该循环必须被垃圾收集。详情请见this answer。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-12-03
    • 2014-11-19
    • 2013-02-15
    • 2011-01-21
    • 2015-12-19
    • 2021-03-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多