Skip to main content

缓冲区协议

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

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

Python以 缓冲协议 的形式在C级提供这样的设施。此协议有两个方面:

  • 在生产者端,类型可以导出“缓冲接口”,其允许该类型的对象暴露关于它们的底层缓冲器的信息。此接口在 缓冲区对象结构 部分中描述;

  • 在消费者方面,几种手段可用于获得指向对象的原始基础数据(例如方法参数)的指针。

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

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

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

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

缓冲结构

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

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

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

Py_buffer
void *buf

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

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

void *obj

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

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

Py_ssize_t len

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

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

int readonly

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

Py_ssize_t itemsize

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

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

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

如果 shape 作为 PyBUF_SIMPLEPyBUF_WRITABLE 请求的结果是 NULL,则消费者必须忽略 itemsize 并假定 itemsize == 1

const char *format

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

此字段由 PyBUF_FORMAT 标志控制。

int ndim

存储器表示为n维数组的维数。如果它是 0buf 指向表示标量的单个项目。在这种情况下,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 需要特别注意。有关详细信息,请参阅 complex arrays

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

Py_ssize_t *strides

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

步幅值可以是任何整数。对于常规数组,步长通常是正的,但是消费者必须能够处理 strides[n] <= 0 的情况。有关详细信息,请参阅 complex arrays

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

Py_ssize_t *suboffsets

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

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

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

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

void *internal

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

缓冲区请求类型

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

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

请求无关字段

以下字段不受 flags 的影响,并且必须始终使用正确的值填充:objbuflenitemsizendim

readonly,格式

PyBUF_WRITABLE

控制 readonly 字段。如果设置,输出器必须提供可写缓冲区或报告失败。否则,导出器可以提供只读或可写缓冲区,但是选择必须对所有消费者是一致的。

PyBUF_FORMAT

控制 format 字段。如果设置,此字段必须正确填写。否则,此字段必须是 NULL

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

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

形状,步幅,子步

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

请求

形状

步幅

子步

PyBUF_INDIRECT

如果需要的话

PyBUF_STRIDES

空值

PyBUF_ND

空值

空值

PyBUF_SIMPLE

空值

空值

空值

连续性请求

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

请求

形状

步幅

子步

重叠群

PyBUF_C_CONTIGUOUS

空值

C

PyBUF_F_CONTIGUOUS

空值

F

PyBUF_ANY_CONTIGUOUS

空值

C或F

PyBUF_ND

空值

空值

C

复合请求

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

在下表中,U 表示未定义的邻接。消费者将不得不呼叫 PyBuffer_IsContiguous() 来确定连续性。

请求

形状

步幅

子步

重叠群

只读

格式

PyBUF_FULL

如果需要的话

U

0

PyBUF_FULL_RO

如果需要的话

U

1或0

PyBUF_RECORDS

空值

U

0

PyBUF_RECORDS_RO

空值

U

1或0

PyBUF_STRIDED

空值

U

0

空值

PyBUF_STRIDED_RO

空值

U

1或0

空值

PyBUF_CONTIG

空值

空值

C

0

空值

PyBUF_CONTIG_RO

空值

空值

C

1或0

空值

复杂数组

NumPy风格:形状和步幅

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

如果 ndim == 0buf 指向的存储器位置被解释为大小为 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风格:形状,步幅和subboffets

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

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

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