Python:修改列表时的内存使用与优化
问题
我有个担心:我在一个普通的 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 可能非常大,任何每次运行只遍历一次的解决方案可能都会更好。
使用 filter 函数:
while processingdata:
for i, item in enumerate(somelist):
dosomestuff(item)
somelist = filter(lambda x: not subtle_condition(x), somelist)
这种方法同样会创建一个新列表,占用大量内存。
使用 itertools 的 filter 函数:
from itertools import ifilterfalse
while processingdata:
for item in itertools.ifilterfalse(somecondtion, somelist):
dosomestuff(item)
这种版本的 filter 调用不会创建新列表,但不会对每个项目调用 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)。我不想走这条路,因为这会需要在几乎所有地方进行大量的代码重构。
6 个回答
在Python中,列表里存储的其实是对象的引用,而不是对象本身。也就是说,当你一个一个地往列表里添加元素时,列表(也就是存储这些对象引用的列表)会逐渐变大,直到用完Python预留的额外内存。然后,Python会把这个列表(引用的列表)复制到一个更大的地方,而你的列表元素仍然保留在原来的位置。因为你的代码会访问旧列表中的所有元素,所以把引用复制到新列表中(通过new_list[i]=old_list[i])几乎不会增加负担。唯一需要注意的是,最好一次性分配所有的新元素,而不是一个一个地添加。不过,Python的文档也提到,随着列表大小的增加,追加元素的平均时间复杂度仍然是O(1)。如果你没有足够的空间来存放新的引用列表,那就麻烦了——任何能够避免O(n)的就地插入或删除的数据结构,可能都会比简单的4字节或8字节的数组要大。
根据你的描述,听起来你需要的正是一个双端队列(deque,读作“deck”)。
http://docs.python.org/library/collections.html#deque-objects
你可以通过不断调用 pop() 来“遍历”这个双端队列。如果你想把弹出的项目保留在队列里,可以用 appendleft(item) 把它放回到前面。为了知道什么时候遍历结束,确保你看过队列里的所有东西,你可以放一个标记,比如 None,来提醒自己,或者在开始某个循环时先获取队列的长度 len(),然后用 range() 来精确弹出那么多个项目。
我相信你会发现你需要的所有操作都是 O(1) 的,也就是说它们的执行速度非常快。
在不知道你具体在做什么的情况下,很难说出最好的方法。如果你的处理步骤需要知道列表中当前元素的位置,那就不适用这个方法。不过,如果不需要的话,你似乎忽略了最符合Python风格(而且在很多方面也最简单)的方法:生成器。
如果你只是想逐个遍历每个元素,对它进行某种处理,然后决定是否把这个元素放入列表中,那就用生成器吧。这样你就不需要把整个可迭代对象都存储在内存里。
def process_and_generate_data(source_iterable):
for item in source_iterable:
dosomestuff(item)
if not somecondition(item):
yield item
你需要有一个处理循环来处理保存处理后的可迭代对象(比如写回文件,或者其他方式),或者如果你有多个处理步骤想分开成不同的生成器,你可以让你的处理循环把一个生成器传递给下一个。