我正在尝试找到一种节省空间的方法来在Python中存储类似结构的对象。
# file point.py
import collections
Point = collections.namedtuple('Point', ['x', 'y'])
这是cythonized版本:
# file cpoint.pyx
cdef class CPoint:
cdef readonly int x
cdef readonly int y
def __init__(self, int x, int y):
self.x = x
self.y = y
def __repr__(self):
return 'Point(x={}, y={})'.format(self.x, self.y)
我希望cythonized版本的内存效率更高:
from pympler.asizeof import asizeof
from point import Point
from cpoint import CPoint
asizeof(Point(1,2)) # returns 184
asizeof(CPoint(1,2)) # returns 24
但令人惊讶的是,尽管静态打字和内存表现较轻,但cython化版本在腌制时会占用更多空间。
import pickle
len(pickle.dumps(Point(1,2))) # returns 28
len(pickle.dumps(CPoint(1,2))) # returns 70
有没有更有效的方法来序列化像这样的cython对象?
我想保留单个CPoint
对象的原因是因为我在流式应用程序中收到类似异性CPoint
的对象,所以我需要在{{1}中缓冲它们异类型。
如果我们对列表元素的类型有保证,确实可以使用numpy数组来改善存储空间。我们也可能会使用同质容器获得更好的压缩属性,但您必须放弃序列化非结构化数据的多功能性。
一种算法解决方案可以回归@ead和@DavidW提出的同质容器的空间优势,同时适应非结构化数据将存储前面对象位置的位图(假设我们知道所有可能的类型在字节码编译时传入的对象,这是一个广泛的假设),然后仍然将对象分组在同质容器中。也许通过以列为导向的方式对它们进行排序可以提高效率,从而使压缩效果更好。没有基准测试就很难说。
答案 0 :(得分:2)
这不是一个特别的Cython解决方案,但是:大概如果你担心磁盘上的大小,那么你有很多这些。在这种情况下,一个好的选择是将数据存储在numpy structured array中,以避免创建大量的Python对象(或者可能是像Pandas那样)。
我还希望腌制一个数组/ numpy对象列表是一个更有用的大小表示而不是腌制一个对象(我相信pickle
做了一些优化,当你有很多同样的事情)
import collections
from cpoint import CPoint
Point = collections.namedtuple('Point', ['x', 'y'])
l = [ Point(n,n) for n in range(10000) ]
l2 = [ CPoint(n,n) for n in range(10000) ]
import numpy as np
l3 = np.array(list(zip(list(range(10000)), list(range(10000)))),
dtype=[('x',int),('y',int)])
import pickle
print("Point",len(pickle.dumps(l))/20000)
print("CPoint",len(pickle.dumps(l2))/20000)
print("nparray",len(pickle.dumps(l3))/20000)
打印:
点9.9384
CPoint 16.4402
nparray 8.01215
namedtuple
和numpy.array
版本都非常接近我们期望的每个int限制8个字节,但numpy数组版本更好。
有趣的是,如果我们在调用中添加protocol=pickle.HIGHEST_PROTOCOL
,那么一切都会进一步改善,namedtuple
版本会再次令人信服地获胜。 (我怀疑它注意到它不需要完整的64位整数存储,我怀疑这是否容易被手动击败)
点5.9775
CPoint 10.47975
nparray 8.0107
答案 1 :(得分:1)
一方面,这个答案应该是对@DavidW答案的补充,但另一方面它也会调查可能的改进。它还建议使用包装器进行序列化,这将保留心爱的CPoint对象,但实现与结构化numpy-arrays相同的密集序列化。
正如已经指出的那样,比较单个序列化对象的大小并没有多大意义 - 这只是过多的开销。除此之外,Python必须保存类的标识符,这是模块+类名的全名。在我的情况下,我使用ipython和%% cython-magic,它很长:
>>> print(pickle.dumps(CPoint(1,2)))
b'\x80\x03c_cython_magic_46e1a18d1df9b5ea5ee974991f9aba67\n__pyx_unpickle_CPoint\nq\x00c_cython_magic_46e1a18d1df9b5ea5ee974991f9aba67\nCPoint\nq\x01J\xe9\x1a\x8d\x0cK\x01K\x02\x86q\x02\x87q\x03Rq\x04.'
自动创建的模块名称的长度为c_cython_magic_46e1a18d1df9b5ea5ee974991f9aba67
并且很痛!
所以基本上,如果不知道你的对象是如何存储的(列表,地图,集合或其他东西),就无法给出正确的答案。
然而,类似于@DavidW,a将假设这些点存储在列表中。当列表中有多个CPoint
个对象时,pickle
足够聪明,只能保存一次Class-header。
我选择了一个稍微不同的测试设置 - 坐标是从[-2e9,2e9]
范围内随机选择的,它基本上涵盖了整个int32范围(很高兴知道,pickle
足够聪明减少小值所需的字节数,但增益的大小取决于点的分布):
N=10000
x_lst=[random.randint(-2*10**9, 2*10**9) for _ in range(N)]
y_lst=[random.randint(-2*10**9, 2*10**9) for _ in range(N)]
并比较Point
s,CPoint
和int32
- 结构化numpy数组的列表:
lst_p = [ Point(x,y) for x,y in zip(x_lst, y_lst)]
lst_cp = [ CPoint(x,y) for x,y in zip(x_lst, y_lst)]
lst_np = np.array(list(zip(x_lst, y_lst)), dtype=[('x',np.int32),('y',np.int32)])
产生以下结果:
print("Point", len(pickle.dumps(lst_p,protocol=pickle.HIGHEST_PROTOCOL))/N)
print("CPoint", len(pickle.dumps(lst_cp,protocol=pickle.HIGHEST_PROTOCOL))/N)
print("nparray", len(pickle.dumps(lst_np,protocol=pickle.HIGHEST_PROTOCOL))/N)
Point 16.0071
CPoint 25.0145
nparray 8.0213
这意味着nparray每个条目只需要8个字节(与@DavidW' s答案不同,我看的是整个对象的大小,而不是每个整数值),这与它得到的一样好。这是因为我使用np.int32
而不是int
(通常是64个咬)作为坐标。
重要的一点是:numpy-arrays仍然比Point
的列表更好,即使它们只有很小的坐标 - 在这种情况下,大小约为12个字节,就像@DavidW的实验一样已经表明了。
但是人们可能比CPoint-objects更喜欢CPoint-objects。那么我们还有哪些其他选择?
一个简单的可能性就是不使用自动创建的酸洗功能,而是手工完成:
%%cython
cdef class CPoint:
...
def __getstate__(self):
return (self.x, self.y)
def __setstate__(self, state):
self.x, self.y=state
现在:
>>> pickle.loads(pickle.dumps(CPoint(1,3)))
Point(x=1, y=3)
>>> print("CPoint", len(pickle.dumps(lst_cp,protocol=pickle.HIGHEST_PROTOCOL))/N)
CPoint 18.011
比Point
还差2个字节,但比原始版本好7个字节。一个加号也是我们可以从较小的整数中获得较小的大小 - 但仍然会比Point
版本低2个字节。
另一种方法是定义一个专用的CPoints-class / wraper列表:
%%用Cython
导入阵列
cdef类CPointListWrapper:
cdef list lst
def init (self,lst):
self.lst = LST
def release_list(self):
result=self.lst
self.lst=[]
return result
def __getstate__(self):
output=array.array('i',[0]*(2*len(self.lst)))
for index,obj in enumerate(self.lst):
output[index*2] =obj.x
output[index*2+1]=obj.y
return output
def __setstate__(self, in_array):
self.lst=[]
n=len(in_array)//2
for i in range(n):
self.lst.append(CPoint(in_array[2*i], in_array[2*i+1]))
这显然是快速和肮脏的,并且可以在性能方面提高很多,但我希望你能得到这个要点!现在:
>>> print("CPointListWrapper", len(pickle.dumps(CPointListWrapper(lst_cp),protocol=pickle.HIGHEST_PROTOCOL))/N)
CPoint 8.0149
和numpy一样好但坚持CPoint-objects!它也正常工作:
>>> pickle.loads(pickle.dumps(CPointListWrapper([CPoint(1,2), CPoint(3,4)]))).release_list()
[Point(x=1, y=2), Point(x=3, y=4)]