缓冲区协议

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

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

Python 以 缓冲区协议 的形式在 C 级别提供这种功能。此协议有两方面

  • 在生产者方面,类型可以导出“缓冲区接口”,从而使该类型的对象可以公开有关其底层缓冲区的信息。此接口在 缓冲区对象结构 部分中进行了描述;

  • 在使用者方面,有多种方法可以获取指向对象原始底层数据的指针(例如,方法参数)。

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

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

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

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

缓冲区结构

缓冲区结构(或简称为“缓冲区”)作为一种将来自另一个对象的二进制数据暴露给 Python 程序员的方式非常有用。它们还可以用作零复制切片机制。利用它们引用一块内存的能力,可以很容易地将任何数据暴露给 Python 程序员。该内存可以是 C 扩展中的一个大型常量数组,可以是操作前传递给操作系统库的原始内存块,也可以用于以其本机内存格式传递结构化数据。

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

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

type Py_buffer
自 3.11 版本以来,是 稳定 ABI 的一部分(包括所有成员)。
void *buf

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

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

PyObject *obj

对导出对象的新引用。 此引用归使用者所有,并通过 PyBuffer_Release() 自动释放(即,引用计数递减)并设置为 NULL。 该字段等效于任何标准 C-API 函数的返回值。

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

Py_ssize_t len

product(shape) * itemsize。 对于连续数组,这是底层内存块的长度。 对于非连续数组,如果将逻辑结构复制到连续表示形式,则它是该逻辑结构的长度。

仅当通过保证连续性的请求获取缓冲区时,访问 ((char *)buf)[0] ((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 来浏览缓冲区。

如果由于 PyBUF_SIMPLEPyBUF_WRITABLE 请求导致 shapeNULL,则使用者必须忽略 itemsize 并假设 itemsize == 1

char *format

一个以 NULL 结尾的字符串,采用 struct 模块风格语法,描述单个项目的内容。如果此值为 NULL,则假设为 "B"(无符号字节)。

此字段由 PyBUF_FORMAT 标志控制。

int ndim

内存表示为 n 维数组的维度数。如果为 0,则 buf 指向表示标量的单个项目。在这种情况下,shapestridessuboffsets 必须为 NULL。最大维度数由 PyBUF_MAX_NDIM 给出。

Py_ssize_t *shape

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

形状值被限制为 shape[n] >= 0shape[n] == 0 的情况需要特别注意。有关更多信息,请参阅复杂数组

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

Py_ssize_t *strides

一个长度为 ndimPy_ssize_t 数组,给出在每个维度中跳过多少字节才能到达新元素。

步长值可以是任何整数。对于规则数组,步长通常为正数,但是使用者必须能够处理 strides[n] <= 0 的情况。有关更多信息,请参阅复杂数组

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

Py_ssize_t *suboffsets

一个长度为 ndimPy_ssize_t 数组。如果 suboffsets[n] >= 0,则沿第 n 个维度存储的值是指针,并且子偏移值决定在取消引用后向每个指针添加多少字节。负的子偏移值表示不应发生取消引用(在连续的内存块中步进)。

如果所有子偏移量均为负数(即不需要取消引用),则此字段必须为 NULL(默认值)。

Python Imaging Library (PIL) 使用这种类型的数组表示形式。有关如何访问此类数组的元素,请参阅复杂数组

子偏移数组对于使用者是只读的。

void *internal

这供导出对象在内部使用。例如,导出器可能会将其重新转换为整数,并用于存储有关是否必须在释放缓冲区时释放形状、步长和子偏移数组的标志。使用者不得更改此值。

常量

PyBUF_MAX_NDIM

内存表示的最大维度数。导出器必须遵守此限制,多维缓冲区的使用者应该能够处理多达 PyBUF_MAX_NDIM 个维度。当前设置为 64。

缓冲区请求类型

缓冲区通常通过 PyObject_GetBuffer() 向导出对象发送缓冲区请求来获取。由于内存的逻辑结构的复杂性可能差异很大,因此使用者使用 flags 参数来指定它可以处理的确切缓冲区类型。

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

与请求无关的字段

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

只读,格式

PyBUF_WRITABLE

控制 readonly 字段。如果设置,导出器必须提供可写缓冲区,否则报告失败。否则,导出器可以提供只读或可写缓冲区,但对于所有使用者,选择必须一致。例如,PyBUF_SIMPLE | PyBUF_WRITABLE 可用于请求简单的可写缓冲区。

PyBUF_FORMAT

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

PyBUF_WRITABLE 可以与下一节中的任何标志进行“或”运算。由于 PyBUF_SIMPLE 定义为 0,因此 PyBUF_WRITABLE 可以用作独立标志来请求简单的可写缓冲区。

PyBUF_FORMAT 必须与除了 PyBUF_SIMPLE 之外的任何标志进行“或”运算,因为后者已经隐含了格式 B (无符号字节)。 PyBUF_FORMAT 不能单独使用。

形状、步长、子偏移量

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

请求

形状

步长

子偏移量

PyBUF_INDIRECT

如果需要

PyBUF_STRIDES

NULL

PyBUF_ND

NULL

NULL

PyBUF_SIMPLE

NULL

NULL

NULL

连续性请求

可以显式请求 C 或 Fortran 连续性,无论是否具有步长信息。如果没有步长信息,则缓冲区必须是 C 连续的。

请求

形状

步长

子偏移量

连续

PyBUF_C_CONTIGUOUS

NULL

C

PyBUF_F_CONTIGUOUS

NULL

F

PyBUF_ANY_CONTIGUOUS

NULL

C 或 F

PyBUF_ND

NULL

NULL

C

复合请求

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

在下表中,U 表示未定义的连续性。使用者必须调用 PyBuffer_IsContiguous() 来确定连续性。

请求

形状

步长

子偏移量

连续

只读

格式

PyBUF_FULL

如果需要

U

0

PyBUF_FULL_RO

如果需要

U

1 或 0

PyBUF_RECORDS

NULL

U

0

PyBUF_RECORDS_RO

NULL

U

1 或 0

PyBUF_STRIDED

NULL

U

0

NULL

PyBUF_STRIDED_RO

NULL

U

1 或 0

NULL

PyBUF_CONTIG

NULL

NULL

C

0

NULL

PyBUF_CONTIG_RO

NULL

NULL

C

1 或 0

NULL

复杂数组

NumPy 样式:形状和步长

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 样式:形状、步长和子偏移量

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

这是一个函数,当存在非 NULL 步长和子偏移量时,返回指向由 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;
}