如何将 numpy 数组转换为字节/BytesIO 而不复制?

10
我想使用boto3包将numpy数组上传到S3,但该包需要bytes对象。我想将此numpy数组转换为bytes,但由于内存限制,不希望进行任何复制。以下是我尝试过的一些方法,但它们都不能正常工作,因为它们会创建副本: 似乎numpy曾经提供numpy.ndarray.getbuffer,但在后续版本中已被弃用。
有没有一种方法可以创建一个字节视图而不进行复制?

我认为这个链接 https://dev59.com/CoPba4cB1Zd3GeqPrWOm#25837662 会有所帮助。 - CodeNoob
不幸的是,tostring是tobytes的别名,同时也会创建原始缓冲区的副本。 - franklsf95
我想知道你是如何检查是否被复制的?我也指的是另一种选择,即先保存数组,然后以字节形式读取它。或者使用pickle的方法。 - CodeNoob
你可以通过设置arr[0]=something else来检查,然后再检查缓冲区是否发生变化。我也不想涉及磁盘I/O :) - franklsf95
1个回答

1
你可以利用ctypes模块创建一个指向数据数组的指针,并将其转换为字节形式。
import ctypes

import numpy as np

# generate the test array 
size = 0x10
dtype = np.short
bsize = 2 # size of a single np.short in bytes, set for the data type you want to upload
arr = np.arange(size, dtype=dtype)

# create a pointer to the block of memory that the array lives in, cast to char type. Note that (size*bsize) _must_ be in parenthesis for the code to run correctly.
memory_block = (ctypes.c_char*(size*bsize)).from_address(arr.ctypes.data)
print(memory_block.raw)
# b'\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\t\x00\n\x00\x0b\x00\x0c\x00\r\x00\x0e\x00\x0f\x00'

# mutate the array and check the contents at the pointer
arr[0] = 255.
print(memory_block.raw)
# b'\xff\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x00\x07\x00\x08\x00\t\x00\n\x00\x0b\x00\x0c\x00\r\x00\x0e\x00\x0f\x00'

至少从评论中提出的问题来看,这似乎是满足测试要求的。(即如果我改变数组会不会改变我的视图?)。

但是需要注意几点。首先,Python 字节对象是不可变的,这意味着如果将其分配给一个变量,会生成一份副本。

y = memory_block.raw
print(y[:2])
# b'\xff\x00'
arr[0] = 127
print(y[:2])
# b'\xff\x00'

两个,boto3 似乎需要一个类似文件的对象,至少根据版本 1.28.1 的源代码是这样的。调用 bio = BytesIO(memory_block.raw) 会产生一次复制,这意味着我们又回到了上传的起点。

一个上传器类

下面的 ArrayUploader 类实现了一些基本的 IO 方法(readseektell)。当调用 read 时,数据可能仍然会从底层内存块复制,这意味着头部空间仍然是限制因素。然而,如果设置读取大小,那么只有这么多数据会从内存块中一次性复制出来。我无法告诉你 boto3 如何处理从 IO 对象读取的大小。

import ctypes
import re
from io import IOBase

import numpy as np

class ArrayUploader(IOBase):
    # set this up as a child of IOBase because boto3 wants an object
    # with a read method. 
    def __init__(self, array):
        # get the number of bytes from the name of the data type
        # this is a kludge; make sure it works for your case
        dbits = re.search('\d+', str(np.dtype(array.dtype))).group(0)
        dbytes = int(dbits) // 8
        self.nbytes = array.size * dbytes
        self.bufferview = (ctypes.c_char*(self.nbytes)).from_address(array.ctypes.data)
        self._pos = 0

    def tell(self):
        return self._pos

    def seek(self, pos):
        self._pos = pos

    def read(self, size=-1):
        if size == -1:
            return self.bufferview.raw[self._pos:]
        old = self._pos
        self._pos += size
        return self.bufferview.raw[old:self._pos]
    

# generate the test array 
size = 0x10
dtype = np.short
arr = np.arange(size, dtype=dtype)

# initialize our uploader object
arrayuploader = ArrayUploader(arr)

# read some data out
print(x:=arrayuploader.read(8))
# b'\x00\x00\x01\x00\x02\x00\x03\x00'

# mutate the array, reread the same data
arr[0] = 127
arrayuploader.seek(0)
print(y:=arrayuploader.read(8))
# b'\x7f\x00\x01\x00\x02\x00\x03\x00'

# has x changed with the original array?
print(x == y)
# False

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接