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

时间:2010-04-13 15:54:08

标签: python optimization memory list iteration

问题

我关注的是:我在一个经典的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语句花费更多,至少在记忆方面。请记住,某些列表可能非常庞大,并且每次运行只会迭代一次的任何解决方案都可能永远胜利。

使用过滤功能:

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)。我不想去这里,因为这需要大量的代码重构几乎无处不在。

7 个答案:

答案 0 :(得分:5)

在不知道您对此列表所做的具体细节的情况下,很难确切知道在这种情况下哪种方法最好。如果您的处理阶段取决于列表元素的当前索引,这将不起作用,但如果不是,则表明您已经离开了最Pythonic(并且在许多方面,最简单)的方法:生成器。

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

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

你需要有一个处理循环来处理持久化已处理的迭代(将其写回文件,或其他任何东西),或者如果你有多个处理阶段,你宁愿分成不同的生成器,你可以拥有你的处理循环将一个生成器传递给下一个。

答案 1 :(得分:3)

根据你的描述,它听起来像一个双端(“牌组”)正是你想要的:

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

通过反复调用pop()来“迭代”它,然后,如果你想将弹出的项目保留在双端队列中,则使用appendleft(item)将该项目返回到前面。为了跟上你完成迭代并看到deque中的所有内容,要么放入一个像你看的标记对象,要么只是在你启动一个特定的循环和使用范围时要求deque的len()( )pop()确切地说那么多项。

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

答案 2 :(得分:2)

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

答案 3 :(得分:2)

双重链接列表比仅重新分配列表更糟糕。 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)

答案 4 :(得分:1)

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

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

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

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

我必须看到测试相信,双向链表实现的内存使用的常数因子将小于仅通过创建新列表获得的2。我真的很怀疑。

我想,你必须分享更多关于你的问题类的更具体的答案,但一般的建议是

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

答案 5 :(得分:1)

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

如果您使用双端队列,我会稍微采用与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

这样做没有明显的记忆差异,但是,与你去的相同的双端变异相比,流失的机会要少得多。

答案 6 :(得分:0)

set(甚至是一个词典)可能就是你要找的东西。它与字典具有相同的底层结构(没有关联的值),但您的对象确实需要可以清除。

如果订单在您的列表/集中很重要,您可以制作有序集。在OrderedSet的activestate上有一个很好的配方。 this answer中还有另一个灵巧的建议。 Python 2.7和3.1也有一个OrderedDict您可以测试自己的实现,看看开销如何影响您,但哈希表的速度增益可能是值得的。

根据您对列表中对象进行的比较,堆(heapq module)也可能适合您的问题。堆将最小化插入和删除基础列表中的项目所需的操作数。