Python:修改列表时的内存使用和优化

Posted

技术标签:

【中文标题】Python:修改列表时的内存使用和优化【英文标题】:Python: Memory usage and optimization when modifying lists 【发布时间】:2011-02-07 12:46:18 【问题描述】:

问题

我关心的是:我将一个相对论大数据集存储在一个经典的 python 列表中,为了处理数据,我必须多次迭代列表,对元素执行一些操作,并且经常弹出一个项目的列表。

似乎从 Python 列表中删除一项需要花费 O(N),因为 Python 必须将现有元素上方的所有项复制到一个位置。此外,由于要删除的项目数与列表中的元素数大致成正比,这导致了 O(N^2) 算法。

我希望找到一种经济高效的解决方案(时间和内存方面)。我研究了我在互联网上可以找到的内容,并在下面总结了我的不同选择。哪一个是最佳人选?

保持本地索引:

while processingdata:
    index = 0
    while index < len(somelist):
        item = somelist[index]
        dosomestuff(item)
        if somecondition(item):
            del somelist[index]
        else:
            index += 1

这是我想出的原始解决方案。这不仅不是很优雅,而且我希望有更好的方法来保持时间和内存效率。

向后遍历列表:

while processingdata:
    for i in xrange(len(somelist) - 1, -1, -1):
        dosomestuff(item)
        if somecondition(somelist, i):
            somelist.pop(i)

这避免了增加索引变量,但最终成本与原始版本相同。它还破坏了 dosomestuff(item) 希望按照它们在原始列表中出现的顺序处理它们的逻辑。

制作新列表:

while processingdata:
    for i, item in enumerate(somelist):
        dosomestuff(item)
    newlist = []
    for item in somelist:
        if somecondition(item):
            newlist.append(item)
    somelist = newlist
    gc.collect()

这是从列表中删除元素的一种非常幼稚的策略,并且需要大量内存,因为必须制作列表的几乎完整副本。

使用列表推导:

while processingdata:
    for i, item in enumerate(somelist):
        dosomestuff(item)
    somelist[:] = [x for x in somelist if somecondition(x)]

这非常优雅,但实际上它会再次遍历整个列表,并且必须复制其中的大部分元素。我的直觉是,至少在内存方面,这个操作可能比原来的 del 语句花费更多。请记住,somelist 可能很大,并且每次运行仅迭代一次的任何解决方案都可能总是获胜。

使用过滤功能:

while processingdata:
    for i, item in enumerate(somelist):
        dosomestuff(item)
    somelist = filter(lambda x: not subtle_condition(x), somelist)

这也会创建一个占用大量 RAM 的新列表。

使用 itertools 的过滤功能:

from itertools import ifilterfalse
while processingdata:
     for item in itertools.ifilterfalse(somecondtion, somelist):
         dosomestuff(item)

此版本的过滤器调用不会创建新列表,但不会对破坏算法逻辑的每个项目调用 dosomestuff。我包含这个示例只是为了创建一个详尽的列表。

边走边上移项目

while processingdata:
    index = 0
    for item in somelist:
        dosomestuff(item)
        if not somecondition(item):
            somelist[index] = item
            index += 1
    del somelist[index:]

这是一种看似划算的巧妙方法。我认为它只会移动每个项目(或指向每个项目的指针?)一次,从而导致 O(N) 算法。最后,我希望 Python 足够智能,可以在最后调整列表大小,而无需为列表的新副本分配内存。不过不确定。

放弃 Python 列表:

class Doubly_Linked_List:
    def __init__(self):
        self.first = None
        self.last = None
        self.n = 0
    def __len__(self):
        return self.n
    def __iter__(self):
        return DLLIter(self)
    def iterator(self):
        return self.__iter__()
    def append(self, x):
        x = DLLElement(x)
        x.next = None
        if self.last is None:
            x.prev = None
            self.last = x
            self.first = x
            self.n = 1
        else:
            x.prev = self.last
            x.prev.next = x
            self.last = x
            self.n += 1

class DLLElement:
    def __init__(self, x):
    self.next = None
    self.data = x
    self.prev = None

class DLLIter:
    etc...

这种类型的对象在一定程度上类似于 python 列表。但是,删除元素的保证是 O(1)。我不想去这里,因为这几乎需要在任何地方进行大量代码重构。

【问题讨论】:

性能比较在哪里? 另外,请注意,当您觉得需要写xrange(len(somelist) - 1, -1, -1) 时,也可以将其写为reversed(xrange(len(somelist))) 【参考方案1】:

如果不知道您使用此列表执行的具体操作,很难确切知道在这种情况下什么是最好的。如果您的处理阶段取决于列表元素的当前索引,这将不起作用,但如果不是,那么您似乎已经放弃了最 Pythonic(并且在许多方面最简单)的方法:生成器。

如果您所做的只是遍历每个元素,以某种方式对其进行处理,然后将该元素包含在列表中或不包含在列表中,请使用生成器。然后,您永远不需要将整个可迭代对象存储在内存中。

def process_and_generate_data(source_iterable):
    for item in source_iterable:
        dosomestuff(item)
        if not somecondition(item):
            yield item

您需要有一个处理循环来处理持久化已处理的可迭代(将其写回文件或其他),或者如果您有多个处理阶段,您希望将其分成不同的生成器,您可以使用您的处理循环将一个生成器传递给下一个生成器。

【讨论】:

【参考方案2】:

你没有提供足够的信息来很好地回答这个问题。我不太了解您的用例,无法告诉您如果必须针对时间进行优化,哪些数据结构将为您带来所需的时间复杂度。典型的解决方案是构建一个新列表而不是重复删除,但这显然会增加一倍(ish)内存使用量。

如果您有内存使用问题,您可能希望放弃使用内存中的 Python 结构并使用磁盘数据库。许多数据库可用,sqlite 随 Python 一起提供。根据您的使用情况和您的内存要求有多紧,array.array 或 numpy 可能会对您有所帮助,但这在很大程度上取决于您需要做什么。 array.array 将具有与 list 相同的时间复杂性,并且 numpy 数组会以一些不同的方式工作。使用惰性迭代器(如生成器和 itertools 模块中的东西)通常可以将内存使用量减少 n 倍。

使用数据库将缩短从任意位置删除项目的时间(但如果这很重要,则会丢失顺序)。使用 dict 也可以做到这一点,但可能会占用大量内存。

您还可以考虑将blist 作为列表的直接替代品,该列表可能会获得您想要的一些妥协。我不相信它会大大增加内存使用量,但它会将项目删除更改为 O(log n)。当然,这是以使其他操作更加昂贵为代价的。

我必须进行测试才能相信双链表实现的内存使用常数因子将小于通过简单创建新列表获得的 2。我真的很怀疑。

我认为,您将不得不分享更多关于您的问题课程以获得更具体的答案,但一般建议是

迭代一个列表以构建一个新列表(或在需要时使用生成器生成项目)。如果您确实需要一个列表,这将具有 2 的内存因子,它可以很好地扩展,但如果您的内存周期不足,则无济于事。 如果内存不足,而不是微优化,您可能需要磁盘数据库或将数据存储在文件中。

【讨论】:

【参考方案3】:

从您的描述看来,双端队列(“deck”)正是您正在寻找的:

http://docs.python.org/library/collections.html#deque-objects

通过重复调用 pop() 来“迭代”它,然后,如果您想将弹出的项目保留在双端队列中,则使用 appendleft(item) 将该项目返回到前面。为了跟上您何时完成迭代并查看了双端队列中的所有内容,请放入您要注意的标记对象(如 None ),或者在启动特定循环并使用 range( ) 到 pop() 正是这么多项目。

我相信你会发现你需要的所有操作都是 O(1)。

【讨论】:

一个更简单的解决方案是从第一个 deque 中弹出并追加到一个 new deque 中,这需要跟踪 less。【参考方案4】:

Python 只存储对列表中对象的引用——而不是元素本身。如果您逐项增长列表,则列表(即对象的引用列表)将一项一项增长,最终到达Python在末尾预分配的多余内存的末尾列表(参考!)。然后它将列表(引用!)复制到一个新的更大的位置,而您的列表元素保留在它们的旧位置。由于您的代码无论如何都会访问旧列表中的所有元素,因此通过 new_list[i]=old_list[i] 将引用复制到新列表将几乎没有负担。唯一的性能提示是一次分配所有新元素而不是附加它们(OTOH Python 文档说摊销附加仍然是 O(1),因为多余元素的数量随着列表大小的增加而增长)。如果您缺少新列表(引用)的位置,那么我担心您不走运 - 任何会逃避 O(n) 就地插入/删除的数据结构都可能比一个简单的 4 数组更大- 或 8 字节条目。

【讨论】:

【参考方案5】:

Brandon Craig Rhodes 建议使用collections.deque,它可以解决这个问题:操作不需要额外的内存,它保持 O(n)。我不知道总内存使用量以及它与列表的比较;值得注意的是,双端队列必须存储更多的引用,如果它不像使用两个列表那样占用大量内存,我不会感到惊讶。您必须测试或研究它才能了解自己。

如果您要使用双端队列,我的部署方式会与 Rhodes 建议的稍有不同:

from collections import deque
d = deque(range(30))
n = deque()

print d

while True:
    try:
        item = d.popleft()
    except IndexError:
        break

    if item % 3 != 0:
        n.append(item)

print n

这样做并没有显着的内存差异,但是与你去变异相同的双端队列相比,失败的机会要少得多。

【讨论】:

我假设“%3”检查是一个占位符,用于模拟原始问题中“somecondition()”的操作,对吧? @Oddthinking,对,我只是应用了一些愚蠢的条件(被三整除的数字被过滤掉了)。 总的来说我还是不太喜欢这个解决方案。充其量它可以帮助你的记忆提高 2 倍,可能要少得多。如果您对 RAM 有那么大的兴趣,那么您应该将数据移动到磁盘上。 (而且因为您需要所有节点来保持对 collections.deque 中任一侧的引用,所以如果 RAM 使用率实际高于使用列表,我不会感到震惊,尽管不那么连续。)【参考方案6】:

双向链表比重新分配列表更糟糕。 Python 列表使用 5 个单词 + 每个元素一个单词。双向链表每个元素使用 5 个单词。即使您使用单链表,它仍然是每个元素 4 个单词 - 比重建列表所需的每个元素少于 2 个单词要糟糕得多。

从内存使用的角度来看,将项目向上移动并删除最后的松弛是最好的方法。如果列表未满一半,Python 将释放内存。要问自己的问题是,这真的很重要。列表条目可能指向一些数据,除非列表中有很多重复的对象,否则列表使用的内存与数据相比是微不足道的。鉴于此,您不妨建立一个新列表。

对于构建新列表,您建议的方法不是那么好。没有明显的理由你不能只浏览一次列表。此外,调用gc.collect() 是不必要的,实际上是有害的——CPython 引用计数无论如何都会立即释放旧列表,甚至其他垃圾收集器在遇到内存压力时也最好收集。所以这样的事情会起作用:

while processingdata:
    retained = []
    for item in somelist:
        dosomething(item)
        if not somecondition(item):
            retained.append(item)
    somelist = retained

如果您不介意在列表推导中使用副作用,那么以下也是一种选择:

def process_and_decide(item):
    dosomething(item)
    return not somecondition(item)

while processingdata:
    somelist = [item for item in somelist if process_and_decide(item)]

inplace方法也可以重构,使机制和业务逻辑分离:

def inplace_filter(func, list_):
    pos = 0
    for item in list_:
        if func(item):
            list_[pos] = item
            pos += 1
    del list_[pos:]

while processingdata:
    inplace_filter(process_and_decide, somelist)

【讨论】:

以上是关于Python:修改列表时的内存使用和优化的主要内容,如果未能解决你的问题,请参考以下文章

读取大型 Excel 工作表时的内存优化

使用内存映射文件MMF实现大数据量导出时的内存优化

如何在 Python 中分析内存使用情况?

如何在 Python 中分析内存使用情况?

内存优化之掌握 APP 运行时的内存模型

声明变量以保存字符串列表时的内存分配