Python dict.setdefault使用更多内存?

时间:2012-08-16 07:16:27

标签: python memory dictionary setdefault

我正在编写一些涉及此类内容的Python代码

values = {}
for element in iterable:
    values.setdefault(element.name, []).append(element)

因为我之前可以对输入进行排序,所以我也像这样实现了它

values = {}

cur_name = None
cur_list = None

for element in iterable:
    if element.name != cur_name:
        values[cur_name] = cur_list
        cur_name = element.name
        cur_list = []
    cur_list.append(element)
if cur_list:
    values[cur_name] = cur_list
del values[None]

此处输入已按element.name排序。

第二种方法比第一种方法快得多,而且它也使用了更少的内存。

这是什么原因?

或者我在第二种方法中犯了某种错误?

3 个答案:

答案 0 :(得分:4)

每次循环时你的原始代码都会创建一个大部分然后被丢弃的列表。它还会进行多次字典查找(查找方法setdefault是字典查找,然后方法本身会进行字典查找,以查看对象是否已设置,以及是否存在另一个存储值)。 .name.append()也是字典查找,但它们仍然存在于修订后的代码中。

for element in iterable:
    values.setdefault(element.name, []).append(element)

修改后的代码仅在名称更改时查找字典,因此它会从每个循环中删除两个字典查找和方法调用。这就是为什么它更快。

至于内存使用,当列表增长时,它有时可能必须复制数据,但如果只能扩展内存块则可以避免这种情况。我的猜测是,创建所有未使用的临时列表可能会使内存更多碎片并强制更多副本。换句话说,Python实际上并没有使用更多内存,但它可能有更多已分配但未使用的内存。

如果您认为setdefault需要考虑使用collections.defaultdict。这可以避免创建列表,除非需要它:

from collections import defaultdict
values = defaultdict(list)
for element in iterable:
    values[element.name].append(element)

这可能仍然比你的第二个代码慢,因为它没有利用你的知识,名字都被分组,但一般情况下它优于setdefault

另一种方法是使用itertools.groupby。像这样:

from itertools import groupby
from operator import attrgetter
values = { name: list(elements) for name,elements in
    groupby(elements, attrgetter('name')) }

利用排序并将所有内容简化为单个词典理解。

答案 1 :(得分:2)

我可以想到为什么第二种方法更快的几个原因。

values.setdefault(element.name, []).append(element)

在这里,您要为每个element创建一个空列表,即使您永远不会使用它。您还为每个元素调用setdefault方法,这相当于一个哈希表查找和一个可能的哈希表写入,加上调用方法本身的成本,这在python中并不是无关紧要的。最后,正如其他人在我发布此答案后指出的那样,您为每个setdefault查找element属性一次,即使它始终引用相同的方法。

在第二个例子中,您可以避免所有这些低效率。您只需要根据需要创建任意数量的列表,并且无需调用任何方法即可完成所有操作,但需要list.append,其中插入了一些字典分配。您也可以通过简单的比较(element.name != cur_name)替换哈希表查找,这是另一项改进。

我希望您也可以获得缓存优势,因为在向列表添加项目时(这会导致大量cache misses),您并没有跳到地方,但是一次只能在一个列表上工作。这样,相关内存可能位于非常接近CPU的高速缓存层中,因此进程更快。不应低估这种影响 - 从RAM中获取数据比从L1缓存(source读取数据慢两个数量级(或约100倍)慢。

当然排序会增加一点时间,但是python拥有世界上最好和最优化的排序算法之一,所有排序算法都用C编码,所以它不会超过上面列出的好处。

我不知道为什么第二种解决方案的内存效率更高。正如Jiri所指出的那样,它可能是不必要的列表,但我的理解是这些应该是垃圾收集器立即收集的,所以它应该只增加一点内存使用量 - 大小一个空列表。也许是因为垃圾收集器比我想象的更懒。

答案 2 :(得分:1)

您的第一个版本有两个效率低下的部分:

  1. 在循环中调用和解除引用 values.setdefault 。你可以在循环之前协调values_setdefault = values.setdefault,它可以加快速度。

  2. 正如其他答案所示,为列表中的每个元素创建新的空列表非常慢且内存效率低下。我不知道如何避免它并立即使用setdefault。