从cStringIO对象创建Numpy数组并避免复制

5 投票
2 回答
1710 浏览
提问于 2025-04-16 20:12

这是为了更好地理解事情。这并不是我需要解决的实际问题。一个 cstringIO 对象应该模拟一个字符串、一个文件,同时也能像迭代器一样遍历每一行。那它是否也模拟了一个缓冲区呢?无论如何,理想情况下,我们应该能够像下面这样构建一个 numpy 数组。

import numpy as np
import cstringIO

c = cStringIO.StringIO('\x01\x00\x00\x00\x01\x00\x00\x00')

#Trying the iterartor abstraction
b = np.fromiter(c,int)
# The above fails with: ValueError: setting an array element with a sequence.

#Trying the file abstraction
b = np.fromfile(c,int)
# The above fails with: IOError: first argument must be an open file

#Trying the sequence abstraction
b = np.array(c, int)
# The above fails with: TypeError: long() argument must be a string or a number 

#Trying the string abstraction
b = np.fromstring(c)
#The above fails with: TypeError: argument 1 must be string or read-only buffer

b = np.fromstring(c.getvalue(), int)  # does work

我的问题是,为什么它会这样表现。

这个问题的实际情况是这样的:我有一个迭代器,它会返回一个元组。我想从这个元组的某个部分创建一个 numpy 数组,并且尽量减少复制和重复。我最开始的做法是把元组中有用的部分写入一个 StringIO 对象,然后用它的内存缓冲区来创建数组。当然,我可以使用 getvalue(),但这会创建并返回一个副本。有什么好的方法可以避免额外的复制呢?

2 个回答

2

因为 cStringIO 不支持缓冲区接口,所以如果它的 getvalue 方法返回的是数据的一个副本,那就没有办法不复制地获取这些数据。

如果 getvalue 直接返回字符串而不复制,那么使用 numpy.frombuffer(x.getvalue(), dtype='S1') 可以得到一个只读的 numpy 数组,这个数组是指向那个字符串的,不需要额外的复制。


为什么 np.fromiter(c, int)np.array(c, int) 不起作用,是因为 cStringIO 在迭代时一次只返回一行,跟文件的行为类似:

>>> list(iter(c))
['\x01\x00\x00\x00\x01\x00\x00\x00']

这样一长串字符串是无法直接转换成一个整数的。

***

除非真的出现问题,否则最好不要太担心复制的问题。原因是,比如使用生成器并传递给 numpy.fromiter 的额外开销,可能比构建一个列表然后传给 numpy.array 的开销还要大——在某些情况下,复制的成本可能比 Python 的运行时开销要低。

不过,如果问题出在内存上,一个解决方案是直接把数据放到最终的 Numpy 数组里。如果你提前知道数组的大小,可以先分配好。如果大小不确定,可以使用 .resize() 方法来根据需要扩展数组。

3

问题似乎出在numpy不喜欢接收字符而不是数字。记住,在Python中,单个字符和字符串是同一种类型——numpy在内部必须有一些类型检测的机制,它把'\x01'当作一个嵌套的序列。

另一个问题是cStringIO是按行迭代的,而不是按字符迭代。

下面这样的迭代器应该能解决这两个问题:

def chariter(filelike):
    octet = filelike.read(1)
    while octet:
        yield ord(octet)
        octet = filelike.read(1)

使用时要这样做(注意要调用seek!):

c.seek(0)
b = np.fromiter(chariter(c), int)

撰写回答