缓冲协议

Python中的某些对象可以访问底层的memoryarray或buffer。这些对象包括内置的bytesbytearray,还有一些扩展类型,比如array.array。第三方图书馆可以为特殊目的定义自己的类型,例如图像处理或数值分析.

虽然这些类型中的每一种都有自己的语义,但它们共享由可能大的内存缓冲区支持的共同特征。在某些情况下,最好是直接访问缓冲区而不进行中间复制.

Python以 bufferprotocol 的形式在C级提供了这样的工具。该协议有两个方面:

  • 在生产者方面,类型可以导出一个“缓冲区接口”,允许该类型的对象公开有关其底层缓冲区的信息。这个接口在缓冲区对象结构;
  • 在消费者方面,有几种方法可以获取指针对象的原始底层数据(例如方法参数).

简单的对象,如bytesbytearray以面向字节的形式公开它们的底层缓冲区。其他形式是可能的;例如,array.array暴露的元素可以是多字节值.

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

缓冲区接口的使用者有两种方法获取目标对象的缓冲区:

  • 使用正确的参数调用PyObject_GetBuffer();
  • 调用PyArg_ParseTuple()(或其中一个兄弟姐妹)用y*, w*s* 格式代码之一.

在这两种情况下,当缓冲区不能调用PyBuffer_Release()时需要了。如果不这样做可能会导致各种问题,例如资源泄漏.

 

缓冲结构

缓冲结构(或简称“缓冲区”)可用作将二进制数据从另一个对象暴露给Python程序员。它们也可以用作零拷贝切片机制。利用它们引用内存的能力,可以轻松地将任何数据公开给Python程序员。内存可以是C扩展中的大型常量数组,它可以是在传递给操作系统库之前进行操作的原始内存块,也可以用于将结构化数据传递给其本机内存格式.

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

关于如何编写导出对象的简短说明,请参阅缓冲区对象结构。要获得缓冲区,请参阅PyObject_GetBuffer().

Py_buffer
void * buf
指向缓冲区所描述的逻辑结构的开始的指针。这可以是导出器的基础物理内存块中的任何位置。例如,负面strides该值可能指向内存块的末尾.

对于连续的数组,该值指向内存块的开头.

void * obj
对导出对象的新引用。引用由消费者拥有并自动递减并设置为NULL PyBuffer_Release()。该字段相当于任何标准C-API函数的返回值.

作为一个特例,对于temporary由包裹的缓冲区PyMemoryView_FromBuffer()要么 PyBuffer_FillInfo()这个字段是NULL。一般情况下,导出对象不得使用此方案.

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

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

int readonly
指示缓冲区是否为只读。该字段由PyBUF_WRITABLE flag.
Py_ssize_t itemsize
单个元素的项目大小(以字节为单位)控制。与非// struct.calcsize()上调用的值相同format values.

重要异常:如果消费者请求没有PyBUF_FORMAT标志的缓冲区,format将设置为NULL,但itemsize仍然具有原始格式的值.

如果shape存在,则相等product(shape) * itemsize == len仍然持有,消费者可以使用itemsize导航缓冲区

如果shapeNULL由于PyBUF_SIMPLEPyBUF_WRITABLE请求,消费者必须忽视itemsize并假设itemsize == 1.

const char * format
A NULstruct模块样式语法中终止字符串,描述单个项目的内容。如果这是NULL, "B"(无符号字节)假设

这个字段是由PyBUF_FORMAT flag.

int ndim
控制的内存表示为n维数组的维数。如果0, buf指向表示标量的单个项目。在这种情况下,shape, stridessuboffsets必须是NULL.

PyBUF_MAX_NDIM限制维度的最大数量64.出口商必须尊重这个限制,多个消费者-dimensionalbuffers应该能够处理最大PyBUF_MAX_NDIM dimensions.

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

形状值限制为shape[n] >= 0。案件shape[n] == 0需要特别注意。有关详细信息,请参阅复杂阵列.

形状数组对于消费者来说是只读的.

Py_ssize_t * strides
Py_ssize_t长度ndim的数组给出要跳过的字节数在eachdimension中获取一个新元素.

两个值可以是任何整数。对于常规阵列,步幅通常是正面的,但消费者必须能够处理strides[n] <= 0的情况。有关详细信息,请参见复杂数组.

strides数组对于消费者来说是只读的.

Py_ssize_t * suboffsets
数组Py_ssize_t的长度ndim。如果suboffsets[n] >= 0,沿第n维存储的值是指针,子偏移值指示在取消引用后要添加到每个指针的字节数。一个负偏移的子偏移值表示不应该发生解引用(跨越连续内存块).

如果所有子偏移都是负数(即不需要解引用),则该字段必须为NULL(默认值)值)。

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

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

void * internal
这是内部使用由出口对象。例如,导出器可能会将此值重新转换为整数,并用于存储标记,以确定缓冲区释放时是否必须同步形状,跨步和子偏移数组。消费者不得改变这个值

 

缓冲请求类型

缓冲区通常是通过PyObject_GetBuffer()向exportobject发送缓冲请求来获得的。由于内存的逻辑结构的复杂性可能会有很大的不同,消费者使用flags参数来指定它可以处理的确切缓冲区类型.

所有Py_buffer字段都是明确的由requesttype.

request-independent fields定义

以下字段不受flags并且必须始终填写正确的值:obj, buf,len, itemsize, ndim.

readonly,格式

PyBUF_WRITABLE
控制readonly领域。如果设置,exporterMUST提供可写缓冲区或报告失败。否则,出口商可以提供只读或可写缓冲区,但选择必须对所有消费者都一致.
PyBUF_FORMAT
控制format字段。如果设置,该字段必须正确填写。否则,这个字段必须是NULL.

PyBUF_WRITABLE可以是下一节中的任何标志。自PyBUF_SIMPLE定义为0,PyBUF_WRITABLE可以用作一个独立的标志来请求一个简单的可写缓冲区.

PyBUF_FORMAT可以是除了PyBUF_SIMPLE以外的任何标志。后者已经暗示了格式B(无符号字节).

形状,步幅,子偏移

控制内存逻辑结构的标志按复杂程度的递减顺序列出。请注意,每个标志包含其下方标志的所有位.

请求 状态 步幅 suboffsets
PyBUF_INDIRECT
如果需要
PyBUF_STRIDES
NULL
PyBUF_ND
NULL NULL
PyBUF_SIMPLE
NULL NULL NULL

 

连续性请求

C或Fortran contiguity 可以明确请求,有或没有步幅信息。没有步幅信息,缓冲区必须是C-连续的

Request shape strides suboffsets contig
PyBUF_C_CONTIGUOUS
NULL Ç
PyBUF_F_CONTIGUOUS
NULL F
PyBUF_ANY_CONTIGUOUS
C或F
PyBUF_ND
是的 NULL NULL C

复合请求

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

在下表中U代表不确定的邻接。消费者可以打电话PyBuffer_IsContiguous()确定邻接度

请求 形状 进步 suboffsets 重叠群 readonly 格式
PyBUF_FULL
如果需要 U 0 是的
PyBUF_FULL_RO
如果需要 U 1或0
PyBUF_RECORDS
U 0 yes
PyBUF_RECORDS_RO
U 1或0
PyBUF_STRIDED
NULL ü 0 NULL
PyBUF_STRIDED_RO
yes NULL U 1或0 NULL
PyBUF_CONTIG
NULL NULL ç 0 NULL
PyBUF_CONTIG_RO
NULL NULL C 1或0 NULL

复杂数组

NumPystyle:形状和步幅

NumPy的逻辑结构样式数组由itemsize,ndim, shapestrides.

定义如果ndim == 0buf指向的内存位置被解释为大小的标量itemsize。在这种情况下,shapestridesNULL.

如果stridesNULL,数组被解释为标准的n维C- 阵列。否则,消费者必须按如下方式访问ann维数组:

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样式:形状,步幅和子偏移

除了常规项之外,PIL样式的数组可以包含必须遵循的指针,以便到达维度中的下一个元素。例如,常规的三维C数组char v[2][2][3]也可以看作是2个指向2个二维数组的数组:char (*v[2])[2][3]。在suboffsets表示中,这两个指针可以嵌入到buf的开头,指向两个char x[2][3]可以位于内存中任何位置的数组

这是一个函数,当存在非NULL步长和子偏移时,返回指向由N维索引指向的ND数组中的元素的指针:

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;
}