如何在不复制的情况下将 numpy 数组转换为 bytes/BytesIO?

fra*_*f95 10 python arrays numpy amazon-s3 boto3

我想使用 boto3 包将 numpy 数组上传到 S3,该包需要一个 bytes 对象。我想将此 numpy 数组转换为字节,但由于内存限制而不进行任何复制。我尝试过的方法不起作用,因为它们会创建副本:

numpy 似乎曾经提供过numpy.ndarray.getbuffer,但在以后的版本中已弃用。

有没有办法在不复制的情况下创建字节视图?

Ell*_*iot 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'
Run Code Online (Sandbox Code Playgroud)

这至少似乎满足了您在问题评论中提出的测试。(即,如果我改变数组,我对它的看法会改变吗?)。

不过,这里有几点需要注意。第一,Python 字节对象是不可变的,这意味着如果将一个对象分配给变量,则会创建一个副本。

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

第二,boto3似乎想要一个类似 File 的对象,至少根据版本 1.28.1 的源代码。调用bio = BytesIO(memory_block.raw)会产生副本,这意味着我们回到了上传的起点。

上传者类

下面的类ArrayUploader实现了一些基本的 IO 方法(readseektell)。当read调用时,数据可能仍然从底层内存 blob 复制,这意味着空间仍然是限制因素。但是,如果设置了读取的大小,则一次仅从内存 blob 复制那么多数据。我无法告诉你如何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
Run Code Online (Sandbox Code Playgroud)