缓冲协议 — Python 文档

来自菜鸟教程
Python/docs/3.9/c-api/buffer
跳转至:导航、​搜索

缓冲协议

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
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 格式 值上调用的 struct.calcsize() 的值相同。

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

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

如果 shapeNULL 作为 PyBUF_SIMPLEPyBUF_WRITABLE 请求的结果,消费者必须忽略 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。 有关详细信息,请参阅 复杂数组

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

Py_ssize_t *suboffsets

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

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

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

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

void *internal

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


缓冲区请求类型

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

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

请求独立字段

以下字段不受 flags 的影响,必须始终填写正确的值:objbuflen、[ X154X]项目大小,ndim


只读,格式

PyBUF_WRITABLE
控制 readonly 字段。 如果设置,导出器必须提供可写缓冲区,否则报告失败。 否则,出口商可以提供只读或可写缓冲区,但选择必须对所有消费者保持一致。
PyBUF_FORMAT
控制 格式 字段。 如果设置,则必须正确填写此字段。 否则,该字段必须为 NULL


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

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


形状、步幅、子偏移

控制存储器逻辑结构的标志按复杂性降序列出。 请注意,每个标志都包含其下方标志的所有位。

要求 形状 大步 子偏移量
PyBUF_INDIRECT
是的 是的 如果需要的话
PyBUF_STRIDES
是的 是的 无效的
PyBUF_ND
是的 无效的 无效的
PyBUF_SIMPLE
无效的 无效的 无效的


邻接请求

C 或 Fortran contiguity 可以明确请求,有和没有步幅信息。 如果没有步幅信息,缓冲区必须是 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 == 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 维索引指向的 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;
}