当我迭代列表时释放内存

Nik*_*Nik 5 python memory-management list

我有一个关于 python 中列表的内存使用情况的假设问题。我有一个很长的列表my_list,如果加载到内存中,它会消耗多个千兆字节。我想循环该列表并在迭代期间仅使用每个元素一次,这意味着我可以在循环它们后从列表中删除它们。当我循环时,我正在内存中存储其他内容,这意味着我分配的内存my_list现在需要用于其他内容。因此,理想情况下,我想在循环遍历列表元素时删除列表元素并释放内存。

I assume, in most cases, a generator would make the most sense here. I could dump my list to a csv file and then read it line by line in a for-loop. In that case, my_list would never be loaded into memory in the first place. However, let's assume for the sake of discussion I don't want to do that.

Is there a way of releasing the memory of a list as I loop over it? The following does NOT work:

>>> my_list = [1,2,3]
>>> sys.getsizeof(my_list)
80
>>> my_list.pop()
>>> sys.getsizeof(my_list)
80
Run Code Online (Sandbox Code Playgroud)

or

>>> my_list = [1,2,3]
>>> sys.getsizeof(my_list)
80
>>> del my_list[-1]
>>> sys.getsizeof(my_list)
80
Run Code Online (Sandbox Code Playgroud)

even when gc.collect() is called explicitly.

The only way that I get to work is copying the array (which at the time of copying would require 2x the memory and thus is again a problem):

>>> my_list = [1,2,3]
>>> sys.getsizeof(my_list)
80
>>> my_list.pop()
>>> my_list_copy = my_list.copy()
>>> sys.getsizeof(my_list_copy)
72
Run Code Online (Sandbox Code Playgroud)

The fact that I don't find information on this topic indicates to me that probably the approach is either impossible or bad practice. If it should not be done this way, what would be the best alternative? Loading from csv as a generator? Or are there even better ways of doing this?

EDIT: as @Scott Hunter pointed out, the garbage collector works for much larger lists:

>>> my_list = [1] * 10**9
>>> for i in range(10):
...     for j in range(10**8):
...         del my_list[-1]
...     gc.collect()
...     print(sys.getsizeof(my_list))
Run Code Online (Sandbox Code Playgroud)

Prints the following:

8000000056
8000000056
8000000056
8000000056
8000000056
4500000088
4500000088
2531250112
1423828240
56
Run Code Online (Sandbox Code Playgroud)

daw*_*awg 3

您在这里的许多假设都是不正确的。

  1. 第一个大问题是假设您可以在使用循环遍历项目时删除项目for你不能。可以使用while以下形式的循环:

    while my_list:
        item=my_list.pop(0)
        process(item)
        # Each my_list[0] element ref_count-- each loop...
        # If the ref_count==0, item is garbage collected automatically
        # But this is very slow with a Python list. 
        # Use a collections.deque instead
    
    
    Run Code Online (Sandbox Code Playgroud)
  2. Python 中的垃圾收集器几乎是无缝且自动的。使用以常见编程模式编写的程序很少需要从程序中调用垃圾收集器。我想不出在我的使用过程中何时需要它。

  3. 为了回答你的问题之一,如果你调用.pop一个列表,从列表中弹出的对象的引用计数就会减少。如果它达到零,该对象就会被自动垃圾收集——无需调用gc.collect().

  4. 调用的主要用途gc.collect()是删除可能混淆默认垃圾收集器的自引用项。(即,a = []; a.append(a); del a。在这种情况下, 的引用计数a永远不会达到 0 并且不会被删除。是一个示例。)

  5. Python(取决于实现)在比列表中通常使用的单个对象大得多的块中分配和释放内存。每个.append.pop来自列表的内容要么进入共享堆,要么进入新的分配或释放。看这里。

  6. 如果您尝试按项目进行内存管理(除非每个项目都很大),那么它的效率将远远低于 Python 的自动内存管理。

  7. 您声明您正在从文件中读取一个大列表,然后将这些项目用于其他用途。只要这些项目没有发生变异——通常这不会产生该项目的新副本。它会导致每个项目的引用计数增加。

  8. 如果您从文件中读取一个大列表,改变每个项目并保留一个副本,那么您的内存使用量确实会增加。但是,当您的第一个列表超出范围时,它会自动删除。

如果您逐行处理文件或使用某种生成器来执行此操作,所有这些问题都没有实际意义。

这是一个例子。假设您想要获取一个文本文件,并且 1)读取每一行;2)将每一行中的“_”改为“:”;3)有一个包含如此处理的每一行的列表。

给定这个 1.3 GB、100,000,000 行的文件:

# Create the file with awk
% awk -v cnt=100000000 'BEGIN{ for (i=1; i<=cnt; i++) print "Line_" i }' >file

% ls -lh file
-rw-r--r--  1 dawg  wheel   1.3G Nov 14 12:34 file
% printf "%s\n...\n%s\n" $(head -n 1 file) $(tail -n 1 file)
Line_1
...
Line_100000000
Run Code Online (Sandbox Code Playgroud)

您可以通过多种不同的方式处理该文件。

首先是你所想的:

from collections import deque 
# If you don't use a deque, this is too SLOW with a Python list

def process(dq):
    rtr=deque()
    while dq:
        line=dq.popleft()
        line=line.rstrip().replace("_", ":")
        rtr.append(line)
        
    return rtr    
        

with open('/tmp/file') as f:
    dq=deque(f.readlines())
    new_dq=process(dq)
    
print(new_dq[-1])  
Run Code Online (Sandbox Code Playgroud)

第二个是使用逐行生成器:

def process(line):
    return line.rstrip().replace("_", ":")

with open('/tmp/file') as f:
    new_li=list(process(line) for line in f)
    
print(new_li[-1])   
Run Code Online (Sandbox Code Playgroud)

第二种方法是 a) 更快;b) 更少的内存 c) 更容易、更Pythonic 编写。

不要过度思考尝试管理内存。Python 比 C 简单得多。