Python 为什么作为列表的文本表示会消耗这么多内存?
我有一个335MB的大文本文件。整个文本被标记化。每个标记之间用空格分隔。我想将每个句子表示为一个单词列表,而整个文本是一个句子列表。这意味着我会得到一个列表 我使用这个简单的代码将文本加载到我的主存中:Python 为什么作为列表的文本表示会消耗这么多内存?,python,memory,Python,Memory,我有一个335MB的大文本文件。整个文本被标记化。每个标记之间用空格分隔。我想将每个句子表示为一个单词列表,而整个文本是一个句子列表。这意味着我会得到一个列表 我使用这个简单的代码将文本加载到我的主存中: def get_tokenized_text(file_name): tokens = list() with open(file_name,'rt') as f: sentences = f.readlines() return [sent.stri
def get_tokenized_text(file_name):
tokens = list()
with open(file_name,'rt') as f:
sentences = f.readlines()
return [sent.strip().split(' ') for sent in sentences]
不幸的是,这种方法消耗了太多的内存,我的笔记本电脑总是崩溃。我有4 GB的内存,但大约5秒钟后就会出现拥塞
为什么??文本应占335 MB左右。即使我很慷慨,并且我已经批准了,比如说,仅仅用于管理的内存就增加了四倍,也没有理由出现内存阻塞。我现在监督内存泄漏吗 列表和字符串是对象,对象的属性占用内存空间。您可以使用
sys.getsizeof
检查对象的大小和开销:
>>> sys.getsizeof('')
49
>>> sys.getsizeof('abcd')
53
>>> sys.getsizeof([])
64
>>> sys.getsizeof(['a'])
72
>>> sys.getsizeof(['a', 'b'])
80
同时在内存中保留数据的多个表示形式。
readlines()
中的文件缓冲区,还有语句
,以及在构建要返回的列表时的文件缓冲区。要减少内存,请一次处理一行文件。只有单词
才能保存文件的全部内容
def get_tokenized_text(file_name):
words = []
f = open(file_name,'rt')
for line in f:
words.extend( x for x in line.strip().split(' ') if x not in words)
return words
words = get_tokenized_text('book.txt')
print words
为什么??文本应占335 MB左右
假设文本是用UTF-8或各种单字节编码之一编码的——这很可能——在Python2中,文本本身确实占用了335MB多一点,但在Python3中至少占用了两倍甚至四倍,这取决于您的实现。这是因为Python3字符串在默认情况下是Unicode字符串,它们在内部表示为每个字符两个或四个字节
即使我很慷慨,并且我已经批准了,比如说,仅仅用于管理的内存就增加了四倍,也没有理由出现内存阻塞
但是有。每个Python对象都有相当大的开销。例如,在CPython 3.4中,有一个refcount、一个指向类型对象的指针、两个将对象链接到双链接列表中的附加指针,以及特定于类型的附加数据。几乎所有这些都是开销。忽略特定类型的数据,在64位构建中,只有三个指针和refcount代表每个对象32字节的开销
字符串具有额外的长度、哈希代码、数据指针和标志,每个对象大约多24个字节(同样假设64位构建)
如果单词的平均长度为6个字符,那么每个单词在文本文件中大约占6个字节,但作为Python对象,大约占68个字节(在32位Python中可能只有40个字节)。这还不包括列表的开销,每个单词可能至少增加8个字节,每个句子可能多增加8个字节
因此,是的,将系数扩大到12或更大似乎根本不太可能
我现在监督内存泄漏吗
不太可能。Python在跟踪对象和收集垃圾方面做得相当好。在纯Python代码中通常看不到内存泄漏。我的第一个答案试图通过不同时在内存中保留中间列表来减少内存使用。 但这仍然无法将整个数据结构压缩到4GB的RAM中 使用这种方法,使用由Project Gutenberg书籍组成的40MB文本文件作为测试数据,数据需求从270 MB减少到55 MB。 一个355MB的输入文件将占用大约500MB的内存,这很有可能是合适的 这种方法构建一个唯一单词的字典,并为每个单词分配一个唯一的整数标记(
word\u dict
)。
然后,句子列表word\u标记
使用整数标记而不是单词本身。
然后,word\u dict
将其键和值交换,以便可以使用word\u tokens
中的整数标记查找相应的单词
我使用的是32位Python,它使用的内存比64位Python少得多,因为指针的大小是64位Python的一半
为了获得list&dictionary等容器的总大小,我使用了Raymond Hettinger的代码。
它不仅包括容器本身,还包括子容器及其指向的底层项目
import sys, os, fnmatch, datetime, time, re
# Original approach
def get_tokenized_text(file_name):
words = []
f = open(file_name,'rt')
for line in f:
words.append( line.strip().split(' ') )
return words
# Two step approach
# 1. Build a dictionary of unique words in the file indexed with an integer
def build_dict(file_name):
dict = {}
n = 0
f = open(file_name,'rt')
for line in f:
words = line.strip().split(' ')
for w in words:
if not w in dict:
dict[w] = n
n = n + 1
return dict
# 2. Read the file again and build list of sentence-words but using the integer indexes instead of the word itself
def read_with_dict(file_name):
tokens = []
f = open(file_name,'rt')
for line in f:
words = line.strip().split(' ')
tokens.append( dict[w] for w in words )
return tokens
# Adapted from http://code.activestate.com/recipes/577504/ by Raymond Hettinger
from itertools import chain
from collections import deque
def total_size(o, handlers={}):
""" Returns the approximate memory footprint an object and all of its contents.
Automatically finds the contents of the following builtin containers and
their subclasses: tuple, list, deque, dict, set and frozenset.
To search other containers, add handlers to iterate over their contents:
handlers = {SomeContainerClass: iter,
OtherContainerClass: OtherContainerClass.get_elements}
"""
dict_handler = lambda d: chain.from_iterable(d.items())
all_handlers = {tuple: iter,
list: iter,
deque: iter,
dict: dict_handler,
set: iter,
frozenset: iter,
}
all_handlers.update(handlers) # user handlers take precedence
seen = set() # track which object id's have already been seen
default_size = sys.getsizeof(0) # estimate sizeof object without __sizeof__
def sizeof(o):
if id(o) in seen: # do not double count the same object
return 0
seen.add(id(o))
s = sys.getsizeof(o, default_size)
for typ, handler in all_handlers.items():
if isinstance(o, typ):
s += sum(map(sizeof, handler(o)))
break
return s
return sizeof(o)
# Display your Python configurstion? 32-bit Python takes about half the memory of 64-bit
import platform
print platform.architecture(), sys.maxsize # ('32bit', 'WindowsPE') 2147483647
file_name = 'LargeTextTest40.txt' # 41,573,429 bytes
# I ran this only for a size comparison - don't run it on your machine
# words = get_tokenized_text(file_name)
# print len(words), total_size(words) # 962,632 268,314,991
word_dict = build_dict(file_name)
print len(word_dict), total_size(word_dict) # 185,980 13,885,970
word_tokens = read_with_dict(file_name)
print len(word_tokens), total_size(word_tokens) # 962,632 42,370,804
# Reverse the dictionary by swapping key and value so the integer token can be used to lookup corresponding word
word_dict.update( dict((word_dict[k], k) for k in word_dict) )
可以那么你的建议是什么呢?一般来说,你不应该一块一块地把大文件读入内存,除非你必须这样做。但如果您这样做,请使用生成器而不是列表。请注意,
sys.getsizeof(['a','b'])
不返回此数据结构正在使用的内存量-它返回为列表分配的内存,不包括其元素。它需要80+50+50=180字节。听起来你不需要把整个内容都读入内存。你应该更清楚地描述你的数据结构以及你想要的结果。这似乎是有希望的。然而,一切都没有改变,笔记本电脑仍然死机。您可能已经减少了内存消耗,但它仍然太大。如果您对重复的单词不感兴趣,请编辑以消除重复的单词。有关此详细说明,请参阅。这是否意味着我处于更宽松的一边,只需要提供更多的内存?或者有没有解决这个问题的策略?这完全取决于你想做什么。您最好的选择可能是找到一种不同的方式来表示您的数据。例如,如果你不坚持把整件事一次记在记忆里,你的境况会好得多。如果您只是将句子表示为单个字符串,而不是列表,根据需要拆分单个句子,而不是预先拆分所有句子,您的境况可能会更好。最终,您拥有了您的磁盘。您总是可以根据需要从磁盘读取数据,并一次处理一行,而不是将整个文件拖到内存中。