Python中Cython对象的节省空间的酸洗?

Python中Cython对象的节省空间的酸洗?,python,struct,cython,pickle,namedtuple,Python,Struct,Cython,Pickle,Namedtuple,我试图找到一种节省空间的方法来在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,

我试图找到一种节省空间的方法来在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
但令人惊讶的是,尽管静态类型和较轻的内存表示,cythonized版本在pickle时占用了更多的空间

import pickle
len(pickle.dumps(Point(1,2)))     # returns 28
len(pickle.dumps(CPoint(1,2)))    # returns 70
有没有一种更有效的方法可以像这样序列化cython对象


随访 我想保留单个
CPoint
对象的原因是因为我在流式应用程序中接收到类似异类
CPoint
的对象,所以我需要将它们缓冲在异类的
列表中

如果我们对列表中的元素类型有保证,那么使用numpy阵列确实可以改善存储占用空间。使用同质容器也可能获得更好的压缩特性,但您必须放弃序列化非结构化数据的多功能性


利用@ead和@DavidW提出的同质容器的空间优势,同时容纳非结构化数据的一种算法解决方案是存储前面对象位置的位图(假设我们在字节码编译时知道所有可能的传入对象类型,这是一个广泛的假设),然后仍然将对象分组到同质容器中。也许可以通过以面向列的方式对它们进行排序来进一步提高效率,以便压缩能够更好地提高效率。很难说没有基准测试。

这不是一个专门的Cython解决方案,但是:如果您担心磁盘上的大小,那么您可能有很多这样的解决方案。在这种情况下,一个好的选择是将数据存储在一个文件中,以避免创建大量Python对象(或者可能创建类似Pandas的对象)

我还希望pickle数组/numpy对象列表是比pickle单个对象更有用的表示大小的方法(我相信
pickle
在有很多相同的东西时会做一些优化)

这张照片是:

第9.9384点

C点16.4402

nparray 8.01215

namedtuple
numpy.array
版本都非常接近我们预期的每整数8字节的限制,但是numpy数组版本更好


有趣的是,如果我们将
protocol=pickle.HIGHEST_protocol
添加到调用中,那么一切都会进一步改进,
namedtuple
版本再次令人信服地获胜。(我怀疑人们注意到它不需要完整的64位整数来存储,我怀疑这是否很容易被手动击败)

第5.9775点

C点10.47975

nparray 8.0107


一方面,这个答案应该是对@DavidW答案的补充,但另一方面,它也调查了可能的改进。它还建议使用包装器进行序列化,这将保留深受喜爱的CPoint对象,但实现与结构化numpy数组相同的密集序列化

正如已经指出的,比较单个序列化对象的大小没有多大意义,这只是太多的开销。除此之外,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
足够聪明,可以只保存一次类头

我选择了一个稍微不同的测试设置-坐标是从范围
[-2e9,2e9]
中随机选择的,该范围基本上覆盖了整个int32范围(很高兴知道,
pickle
足够聪明,可以减少小值所需的字节数,但增益的大小取决于点的分布):

并比较
Point
s、
CPoint
s和
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的回答不同,我看的是整个对象的大小,而不是每个整数值),这是因为我使用
np.int32
而不是
int
(通常是64位)作为坐标

重要的一点是:numpy数组仍然比
点列表更好,即使它们只有很小的坐标-在这种情况下,大小大约为12字节,正如@DavidW的实验所示

但是人们可能更喜欢CPoint对象而不是numpy结构。那么我们还有哪些其他选择呢

一种简单的可能性是不使用自动创建的酸洗功能,而是手动操作:

%%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 
仍然比
差2个字节,但也比原始版本好7个字节。另外,对于较小的整数,我们可以从较小的大小中获益,但仍然比
版本少2个字节

另一种方法是定义CPoints类/包装器的专用列表:

%%赛昂 导入数组
cdef类CPointListWrapper: cdef列表lst definit(自身,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对象!它也能正常工作:

>>> pickle.loads(pickle.dumps(CPointListWrapper([CPoint(1,2), CPoint(3,4)]))).release_list()
[Point(x=1, y=2), Point(x=3, y=4)]

我认为这个实验有点偏颇:
 >>> print("CPointListWrapper", len(pickle.dumps(CPointListWrapper(lst_cp),protocol=pickle.HIGHEST_PROTOCOL))/N)
 CPoint 8.0149
>>> pickle.loads(pickle.dumps(CPointListWrapper([CPoint(1,2), CPoint(3,4)]))).release_list()
[Point(x=1, y=2), Point(x=3, y=4)]