是否有充分的理由认为iter.remove()当前未在python字典中实现?

假设我需要删除集合/字典中大约一半的元素。然后,我不得不:


复制整个集合/词典(n个空间,n次)
遍历副本以查找要删除的元素,将其从原始字典中删除(n / 2加n / 2个不同的查找)


要么:


遍历字典,添加要删除的元素到新集合(n个空格,n次)
遍历新集合,从原始字典中删除每个元素(n / 2加n / 2个查找)


虽然渐近一切都仍为“ O(n)”时间,但效率极差,并且与理智的方式相比,速度慢约三倍:


遍历dict,移除您不需要的内容。这确实是n次,并且是O(1)空间。


至少在哈希集作为链表的存储桶的常见实现方式下,迭代器应该能够通过简单地删除链表中的节点来删除刚访问的元素,而无需进行新的查找。

更重要的是,糟糕的解决方案还需要O(n)空间,即使对于那些倾向于在python中忽略这些优化问题的人来说,这也确实很糟糕。

最佳答案

在比较中,您犯了两个大错误。首先,您甚至忽略了惯用的“不要删除任何内容,只复制一半的字典”选项。其次,您没有意识到在2/3负载下删除哈希表中一半的条目会在1/3负载下留下大小完全相同的哈希表。

因此,让我们比较实际的选择(我将忽略2/3负载以与您的n / 2量度保持一致)。每一个都有峰值空间,最终空间和时间:


2.0n,1.0n,1.5n:复制,删除原件的一半
2.0n,1.0n,1.5n:复制,删除一半副本
1.5n,1.0n,1.5n:建立删除集,然后删除
1.0n,1.0n,0.5n:就地删除一半
1.5n,0.5n,1.0n:原位删除一半,然后压缩
1.5n,0.5n,0.5n:复制一半


因此,您提出的设计将比我们惯用的设计更糟糕。您只是为了节省相等数量的瞬态空间而将最终(永久)空间加倍,或者为同一空间花费了两倍的时间。



同时,特别是如果您使用理解能力,构建新词典意味着:


有效地保持不变(自动线程/过程安全性,参照透明性等)。
较少的地方会犯难以发现和调试的“小”错误。
通常更紧凑,更易读。
语义受限的循环,字典构建和异常处理为优化提供了机会(CPython采取了这种方式;通常,理解速度比显式循环快40%)。




有关如何在CPython中实现字典的更多信息,请参见the source,该文献已被全面记录,即使您不是C专家,也几乎可以很容易阅读。

如果您考虑事情是如何工作的,那么您假定的某些选择显然应该走另一条路—例如,考虑到Python仅将引用存储在容器中,而不是实际值,并尽可能避免malloc开销,因此这样做的几率是多少?将使用链接而不是开放地址?

您可能还想看看PyPy实现,该实现在Python中使用,并且有更多技巧。



在回答您的所有评论之前,您应该记住,StackOverflow并不是考虑或进行Python更改的地方。如果您确实认为应该进行某些更改,则应将其发布在python-ideas,python-dev和/或bug站点上。但在您这样做之前:您显然仍在使用2.x;如果您不愿意学习3.x来获得过去五年来的任何改进或优化,那么当您建议进行其他更改时,那里的任何人都不会认真对待您。另外,熟悉您要更改的结构;一旦您开始基于可能使用链接的Python dict争论,您唯一会得到的答复就是更正。无论如何:




请向我说明“删除一半就位”如何占用1.0n的空间,并在最终空间中增加1.0n的空间。


我无法解释我没有说的话,那是不对的。任何地方都没有“添加”。我的数字是总的峰值空间和总的最终空间。您的算法显然每个都是1.0n。听起来不错,直到将其与最后两个选项(最终空间总计为0.5n)进行比较。


作为您赞成不向程序员提供就地删除选项的论点,


不做出改变的论点永远不会是“改变是不可能的”,很少是“改变本质上是有害的”,而是通常“改变的代价大于收益”。成本是显而易见的:涉及到工作。语言和每种实现方式增加的复杂性; Python版本之间的更多差异;潜在的TOOWTDI违规行为或令人讨厌的滋扰;等等。这些都不意味着没有变化可以进入。对Python进行的几乎每一次更改都几乎包含了所有这些成本。但是,如果变革的好处不值得付出代价,那么就不值得改变。而且,如果收益少于最初显示的收益,因为您希望进行的优化(a)实际上是一种悲观化,而(b)则需要放弃使用其他收益,即使并非如此,这也使您远离酒吧。

另外,我不确定,但是听起来您相信,一种显而易见的,一种做事方式以及一种语言被设计为在可能的情况下鼓励这种明显方式的想法构成Python是“保姆”。如果是这样,那么您将严重使用错误的语言。有些人讨厌试图让他们以Python方式做事,但是他们很聪明,不使用Python,更不用说改变它了。


您的第四个观点与该问题的邮件列表中的观点相呼应,可以很容易地得到解决……通过简单地在mydict.iteritems()中提供“ for(a,b)for iter”,方法与之相同。目前已在“使用open(...)作为文件句柄”上下文中为文件句柄完成。


那将如何“修复”任何东西?听起来就像您通过编写it = iter(mydict.items())然后for (a, b) in it:可以获得完全相同的语义。但是,无论语义是什么,它们将如何为理解所提供的编译器优化提供相同或等效的便捷机会?理解上,您只能从范围中返回一个地方。它总是返回堆栈中已经存在的最高值。除了构造型StopIteration处理程序,在当前范围内不能保证没有异常处理。在构建list / set / dict时,有一系列非常特定的事件序列,可以安全地使用通常不安全且不灵活的操作码来缩短常规行为。您期望如何获得这些优化中的任何一个,更不用说所有这些优化了?


“要么将最终的(永久)空间加倍,以节省等量的瞬态空间,要么为相同的空间花费两倍的时间。”请解释一下您的想法。


这是可行的,因为1.0是0.5的两倍。更具体而言,已扩展到n个元素且现在负载约为1/3的哈希表的大小是已扩展到n / 2个元素且现在负载约为2/3的哈希表的两倍。这不清楚吗?


原位删除占用O(1)空间


好的,如果您要计算额外的最终空间而不是总的最终空间,那么可以,我们可以说就地删除需要0.0n空间,而复制一半需要-0.5n空间。移零点不会改变比较。


而且所有选项都不会花费少于1.0n的时间


抱歉,这可能还不清楚,因为我在这里谈论的是增加的成本,也许不应该,也没有提及。但是同样,改变比例或零点没有任何区别。显然,从一个字典中删除0.5n个密钥所花的时间与向另一个字典中添加0.5n个密钥所花的时间一样多,并且所有其他步骤都是相同的,因此没有时间差。无论您将它们都称为0.5n还是1.0n都相等。


我之所以不考虑只复制字典的一半,是因为要求是实际上要修改字典,正如明确指出的那样。


不,没有明确说明。您所说的只是“我需要删除集合/字典中大约一半的元素”。在99%的用例中,d = {k: v for k, v in d.items() if pred(k)}是编写它的方式。在很多情况下,人们提出了不正确的地方(“但是我需要背景线程来立即查看更改”)是积极的坏主意。当然,这里有一些反例,但您不能期望人们甚至在没有暗示自己可能的情况下就以为您拥有一个。


但是,最后的空间是1.5n,而不是0.5n


不,不是。原始哈希表是垃圾,因此需要清理,因此最终空间只是新的一半大小的哈希表。 (如果那不是真的,那么实际上您仍然需要原始字典以及新字典,在这种情况下,您别无选择,只能先复制。)

而且,如果您要说:“是的,但是直到它被清理干净” —是的,这就是为什么峰值空间是1.5n而不是1.0n的原因,因为两个哈希表都存在一些非零时间。

10-05 19:07