Buffer Protocol

Python中可用的某些对象包装访问底层内存数组或缓冲区这些对象包括内建bytesbytearray以及一些扩展类型,例如array.array第三方库可以为特殊目的定义它们自己的类型,例如图像处理或数字分析。

虽然每个类型都有自己的语义,但是它们具有由可能大的内存缓冲区支持的共同特性。因此,在某些情况下,希望直接访问该缓冲器,而不需要中间复制。

Python以buffer protocol的形式在C级提供了这样的功能。此协议有两个方面:

  • 在生产者端,类型可以导出“缓冲区接口”,其允许该类型的对象暴露关于它们的底层缓冲区的信息。此接口在Buffer Object Structures部分中描述;
  • 在消费者方面,几种手段可用于获得指向对象的原始基础数据(例如方法参数)的指针。

简单对象(例如bytesbytearray)以字节为单位显示其底层缓冲区。其他形式是可能的;例如,由array.array暴露的元素可以是多字节值。

缓冲区接口的示例消费者是文件对象的write()方法:可以通过缓冲区接口导出一系列字节的任何对象都可以写入文件。虽然write()只需要对传递给它的对象的内部内容进行只读访问,但是其他方法如readinto()需要对它们的内容论据。缓冲器接口允许对象选择性地允许或拒绝读写和只读缓冲器的导出。

缓冲器接口的消费者有两种方式来获取目标对象上的缓冲区:

在这两种情况下,当不再需要缓冲区时,必须调用PyBuffer_Release()否则可能导致各种问题,例如资源泄漏。

Buffer structure

缓冲结构(或简称“缓冲器”)作为将二进制数据从另一个对象暴露给Python程序员的方法是有用的。它们也可以用作零拷贝切片机制。使用它们引用一块内存块的能力,可以很容易地向Python程序员公开任何数据。存储器可以是C扩展中的大的,恒定的数组,其可以是用于在传递到操作系统库之前进行操作的原始存储器块,或者其可以用于以其本机的内存格式传递结构化数据。

与由Python解释器暴露的大多数数据类型相反,缓冲区不是PyObject指针,而是简单的C结构。这允许它们被非常简单地创建和复制。当需要缓冲区周围的通用包装器时,可以创建memoryview对象。

有关如何编写导出对象的简短说明,请参见Buffer Object Structures要获取缓冲区,请参阅PyObject_GetBuffer()

Py_buffer
void *buf

指向由缓冲区字段描述的逻辑结构的开始的指针。这可以是导出器的底层物理存储器块内的任何位置。例如,对于负的strides,该值可能指向内存块的末尾。

对于contiguous数组,该值指向内存块的开头。

void *obj

对导出对象的新引用。引用由使用者拥有,并由PyBuffer_Release()自动递减并设置为NULL该字段等价于任何标准C-API函数的返回值。

作为特殊情况,对于由PyMemoryView_FromBuffer()PyBuffer_FillInfo()包装的临时缓冲区,此字段为NULL t7 >。一般来说,导出对象不能使用此方案。

Py_ssize_t len

product(shape) * itemsize对于连续数组,这是底层内存块的长度。对于非连续数组,它是逻辑结构在复制到连续表示时将具有的长度。

访问((char *)buf)[0] *)buf)[len-1]仅在缓冲区通过确保连续性的请求获得时有效。在大多数情况下,这样的请求将是PyBUF_SIMPLEPyBUF_WRITABLE

int readonly

指示缓冲区是否为只读。此字段由PyBUF_WRITABLE标志控制。

Py_ssize_t itemsize

单个元素的项大小(以字节为单位)。struct.calcsize()的值调用非NULL format值相同。

重要例外:如果消费者请求没有PyBUF_FORMAT标志的缓冲区,format将设置为NULL,但itemsize

如果shape存在,则等于乘积(形状) * itemsize / t7> len仍然保持,消费者可以使用itemsize导航缓冲区。

如果作为PyBUF_SIMPLEPyBUF_WRITABLE请求的结果,shapeNULL,则消费者必须忽略itemsize并假设itemsize == 1

const char *format

struct模块样式语法中描述单个项目内容的NUL终止字符串。如果这是NULL,则假设为"B"(无符号字节)。

此字段由PyBUF_FORMAT标志控制。

int ndim

存储器表示为n维数组的维数。如果它为0,则buf指向表示标量的单个项。在这种情况下,shapestridessuboffsets必须为NULL

PyBUF_MAX_NDIM将最大维数限制为64。出口商必须遵守此限制,多维缓冲器的消费者应该能够处理多达PyBUF_MAX_NDIM维度。

Py_ssize_t *shape

长度ndimPy_ssize_t的数组指示作为n维数组的存储器的形状。注意,shape [0] * ... * shape [ndim-1 ] * itemsize必须等于len

形状值限于shape [n] > = 0需要特别注意情况shape [n] == 0有关详细信息,请参见复杂数组

形状数组对于消费者是只读的。

Py_ssize_t *strides

长度为ndimPy_ssize_t数组,给出每个维度中要跳到新元素的字节数。

步幅值可以是任何整数。对于规则数组,步长通常是正的,但消费者必须能够处理strides [n] 0 t3 >有关详细信息,请参见复杂数组

strides数组对于消费者是只读的。

Py_ssize_t *suboffsets

长度为ndimPy_ssize_t数组。如果subboffset [n] > = 0,沿着第n维存储的值是指针, value指示在解除引用之后要添加到每个指针的字节数。一个负的subboff值表示不应该发生取消引用(跨接在连续的内存块中)。

如果所有子步骤都是负的(即无需取消引用,则此字段必须为NULL(默认值)。

这种类型的数组表示由Python Imaging库(PIL)使用。有关如何访问此类数组元素的更多信息,请参见复杂数组

suboffsets数组对于使用者是只读的。

void *internal

这是由导出对象在内部使用。例如,这可能被输出器重新转换为整数,并用于存储关于在缓冲区释放时是否必须释放shape,strides和subboffsets数组的标志。消费者不得更改此值。

Buffer request types

缓冲区通常通过PyObject_GetBuffer()向导出对象发送缓冲请求来获得。由于存储器的逻辑结构的复杂性可能发生显着变化,因此消费者使用flags参数指定其可以处理的确切缓冲区类型。

所有Py_buffer字段由请求类型明确定义。

request-independent fields

The following fields are not influenced by flags and must always be filled in with the correct values: obj, buf, len, itemsize, ndim.

readonly, format

PyBUF_WRITABLE

Controls the readonly field. If set, the exporter MUST provide a writable buffer or else report failure. Otherwise, the exporter MAY provide either a read-only or writable buffer, but the choice MUST be consistent for all consumers.

PyBUF_FORMAT

Controls the format field. If set, this field MUST be filled in correctly. Otherwise, this field MUST be NULL.

PyBUF_WRITABLE可以转到下一节中的任何标志。由于PyBUF_SIMPLE定义为0,因此PyBUF_WRITABLE可用作独立标志来请求简单的可写缓冲区。

PyBUF_FORMAT可以写入除PyBUF_SIMPLE之外的任何标志。后者已暗示格式B(无符号字节)。

shape, strides, suboffsets

控制存储器的逻辑结构的标志以复杂性的降序列出。注意每个标志包含它下面的标志的所有位。

请求形状步幅子步
PyBUF_INDIRECT
如果需要的话
PyBUF_STRIDES
空值
PyBUF_ND
空值空值
PyBUF_SIMPLE
空值空值空值

contiguity requests

C或Fortran contiguity,可以有和没有步幅信息。没有步幅信息,缓冲区必须是C连续的。

请求形状步幅子步重叠群
PyBUF_C_CONTIGUOUS
空值C
PyBUF_F_CONTIGUOUS
空值F
PyBUF_ANY_CONTIGUOUS
空值C或F
PyBUF_ND
空值空值C

compound requests

所有可能的请求都由上一节中的标志的某种组合完全定义。为了方便起见,缓冲器协议提供经常使用的组合作为单个标志。

在下表中U代表未定义的邻接。消费者必须调用PyBuffer_IsContiguous()来确定连续性。

请求形状步幅子步重叠群只读格式
PyBUF_FULL
如果需要的话U0
PyBUF_FULL_RO
如果需要的话U1或0
PyBUF_RECORDS
空值U0
PyBUF_RECORDS_RO
空值U1或0
PyBUF_STRIDED
空值U0空值
PyBUF_STRIDED_RO
空值U1或0空值
PyBUF_CONTIG
空值空值C0空值
PyBUF_CONTIG_RO
空值空值C1或0空值

Complex arrays

NumPy-style: shape and strides

NumPy式数组的逻辑结构由itemsizendimshapestrides定义。

如果ndim == 0,则解释由buf指向的存储器位置作为大小itemsize的标量。在这种情况下,shapestrides都是NULL

如果stridesNULL,则数组被解释为标准n维C阵列。否则,消费者必须访问n维数组,如下所示:

ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1] item = *((typeof(item) *)ptr);

如上所述,buf可以指向实际内存块中的任何位置。输出器可以使用此函数检查缓冲区的有效性:

def verify_structure(memlen, itemsize, ndim, shape, strides, offset):
    """Verify that the parameters represent a valid array within
       the bounds of the allocated memory:
           char *mem: start of the physical memory block
           memlen: length of the physical memory block
           offset: (char *)buf - mem
    """
    if offset % itemsize:
        return False
    if offset < 0 or offset+itemsize > memlen:
        return False
    if any(v % itemsize for v in strides):
        return False

    if ndim <= 0:
        return ndim == 0 and not shape and not strides
    if 0 in shape:
        return True

    imin = sum(strides[j]*(shape[j]-1) for j in range(ndim)
               if strides[j] <= 0)
    imax = sum(strides[j]*(shape[j]-1) for j in range(ndim)
               if strides[j] > 0)

    return 0 <= offset+imin and offset+imax+itemsize <= memlen

PIL-style: shape, strides and suboffsets

除了常规项,PIL样式数组可以包含必须遵循的指针才能到达维中的下一个元素。例如,常规三维C阵列char v [2] [2] [3] 2个指针到2个二维数组:char (* v [2])[2] [3]在子boffets表示中,这两个指针可以嵌入在buf的开始,指向两个char x [2] [3] t5 >可以位于内存中任何位置的数组。

这里是一个函数,它返回一个指针指向一个N维数组指向一个N维索引的元素,当有非NULL的步幅和子步骤:

void *get_item_pointer(int ndim, void *buf, Py_ssize_t *strides,
                       Py_ssize_t *suboffsets, Py_ssize_t *indices) {
    char *pointer = (char*)buf;
    int i;
    for (i = 0; i < ndim; i++) {
        pointer += strides[i] * indices[i];
        if (suboffsets[i] >=0 ) {
            pointer = *((char**)pointer) + suboffsets[i];
        }
    }
    return (void*)pointer;
}