Buffer Protocol

Python 中可用的某些对象包装对基础内存阵列或* buffer *的访问。这样的对象包括内置的bytesbytearray以及某些 extensions,例如array.array。第三方库可以出于特殊目的定义自己的类型,例如图像处理或数值分析。

尽管每种类型都有其自己的语义,但是它们具有共同的 Feature,即可能由较大的内存缓冲区支持。因此,在某些情况下,希望直接访问该缓冲区而无需中间复制。

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

  • 在生产者端,类型可以导出“缓冲区接口”,该接口允许该类型的对象公开有关其基础缓冲区的信息。 缓冲区对象结构部分中描述了此界面;

  • 在 Consumer 方面,可以使用几种方法来获取指向对象原始基础数据的指针(例如,方法参数)。

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

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

缓冲区接口的使用者可以pass两种方式在目标对象上获取缓冲区:

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

Buffer structure

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

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

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

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

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

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

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

  • Py_ssize_t len
    • product(shape) * itemsize。对于连续数组,这是基础内存块的长度。对于非连续数组,它是逻辑结构被复制到连续表示形式后的长度。

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

  • int readonly

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

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

重要 exception:如果使用者请求不带PyBUF_FORMAT标志的缓冲区,则format将设置为NULL,但是itemsize仍具有原始格式的值。

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

如果由于PyBUF_SIMPLEPyBUF_WRITABLE请求而使shapeNULL,则 Consumer 必须忽略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需要特别注意。有关更多信息,请参见complex arrays

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

  • Py_ssize_t * strides
    • 长度为ndimPy_ssize_t数组,给出在每个维度上要跳转到新元素所要跳过的字节数。

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

步幅数组对使用者而言是只读的。

  • Py_ssize_t * suboffsets
    • 长度为ndimPy_ssize_t数组。如果suboffsets[n] >= 0,则沿第 n 维存储的值是指针,并且子偏移量值指示解引用后要向每个指针添加多少字节。负的子偏移值表示不应取消引用(跨越连续内存块)。

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

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

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

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

缓冲请求类型

通常passpassPyObject_GetBuffer()向输出对象发送缓冲区请求来获取缓冲区。由于存储器逻辑结构的复杂性可能会发生巨大变化,因此使用者使用* flags *参数指定它可以处理的确切缓冲区类型。

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

request-independent fields

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

readonly, format

Note

  • PyBUF_WRITABLE

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

  • PyBUF_FORMAT

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

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

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

形状,步幅,子偏移

控制内存逻辑结构的标志以复杂度从高到低的 Sequences 列出。请注意,每个标志都包含其下面的标志的所有位。

Requestshapestridessuboffsets
PyBUF_INDIRECTyesyesif needed
PyBUF_STRIDESyesyesNULL
PyBUF_NDyesNULLNULL
PyBUF_SIMPLENULLNULLNULL

contiguity requests

可以显式请求 C 或 Fortran contiguity(带有和不带有跨步信息)。没有跨步信息,缓冲区必须是 C 连续的。

Requestshapestridessuboffsetscontig
PyBUF_C_CONTIGUOUSyesyesNULLC
PyBUF_F_CONTIGUOUSyesyesNULLF
PyBUF_ANY_CONTIGUOUSyesyesNULLC 或 F
PyBUF_NDyesNULLNULLC

compound requests

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

在下表中,* U *代表未定义的连续性。Consumer 将必须致电PyBuffer_IsContiguous()来确定连续性。

Requestshapestridessuboffsetscontigreadonlyformat
PyBUF_FULLyesyesif neededU0yes
PyBUF_FULL_ROyesyesif neededU1 或 0yes
PyBUF_RECORDSyesyesNULLU0yes
PyBUF_RECORDS_ROyesyesNULLU1 或 0yes
PyBUF_STRIDEDyesyesNULLU0NULL
PyBUF_STRIDED_ROyesyesNULLU1 或 0NULL
PyBUF_CONTIGyesNULLNULLC0NULL
PyBUF_CONTIG_ROyesNULLNULLC1 或 0NULL

Complex arrays

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可以指向实际存储块内的任何位置。导出器可以使用以下Function检查缓冲区的有效性:

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 个二维数组char (*v[2])[2][3]的 2 个指针的数组。在子偏移量表示中,这两个指针可以嵌入在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;
}
  • int PyObject_CheckBuffer(PyObject ** obj *)

    • 如果 obj *支持缓冲区接口,则返回1,否则返回0。返回1时,不能保证PyObject_GetBuffer()将成功。此Function始终成功。
  • int PyObject_GetBuffer(PyObject *exporter Py_buffer view *,int * flags *)

    • 向* exporter 发送请求以填写 flags 指定的* view *。如果导出器不能提供确切类型的缓冲区,则必须提高PyExc_BufferError,将view->obj设置为NULL并返回-1

成功后,填写* view ,将view->obj设置为对 exporter 的新引用,并返回 0.对于将请求重定向到单个对象的链接缓冲区提供程序,view->obj可以引用该对象而不是 exporter *(参见缓冲区对象结构)。

PyObject_GetBuffer()的成功呼叫必须与对PyBuffer_Release()的呼叫配对,类似于malloc()free()。因此,在使用方使用完缓冲区后,PyBuffer_Release()必须被精确地调用一次。

  • 无效PyBuffer_Release(Py_buffer *查看)
    • 释放缓冲区* view *并减少view->obj的引用计数。当不再使用缓冲区时,必须调用此函数,否则可能会发生引用泄漏。

在不是passPyObject_GetBuffer()获得的缓冲区上调用此函数是错误的。

  • Py_ssize_t PyBuffer_SizeFromFormat(const char ***)

  • int PyBuffer_IsContiguous(Py_buffer ** view *,char * order *)

    • 如果* view 定义的内存是 C 样式( order 'C')或 Fortran 样式( order 'F')contiguous或其中一个( order *是'A'),则返回1。否则返回0。此Function始终成功。
  • void * PyBuffer_GetPointer(Py_buffer 视图,Py_ssize_t* 索引*)

    • 获取给定* view 内部的 indices *指向的内存区域。 索引必须指向view->ndim个索引数组。
  • int PyBuffer_FromContiguous(Py_buffer 视图,无效* buf *,Py_ssize_t * len *,char * fort *)

    • 将连续的* len 个字节从 buf 复制到 view *。 * fort *可以是'C''F'(用于 C 样式或 Fortran 样式 Order)。成功返回0,错误返回-1
  • int PyBuffer_ToContiguous(void *buf Py_buffer src *,Py_ssize_t * len *,char * order *)

    • 将* len 个字节从 src 复制到它在 buf *中的连续表示形式。 * order *可以是'C''F''A'(用于 C 样式或 Fortran 样式的 Order,或两者之一)。成功返回0,错误返回-1

如果* len *!= * src-> len *,此函数将失败。

  • 无效PyBuffer_FillContiguousStrides(int * ndims *,Py_ssize_t *shape ,Py_ssize_t strides *,int * itemsize *,char * order *)

    • 用给定形状的contiguous(如果* order 'C'则为 C 样式,如果 order 'F'则为 Fortran 样式)的字节 Span 填充 strides *数组,并且每个元素具有给定的字节数。
  • int PyBuffer_FillInfo(Py_buffer 视图PyObject* Export 商*,void ** buf *,Py_ssize_t * len *,int * readonly *,int * flags *)

    • 为希望公开大小 len buf 且根据 readonly *设置可写性的导出程序处理缓冲区请求。 * buf *被解释为无符号字节序列。
  • flags 参数指示请求类型。除非 buf 被指定为只读并且在 flags 中设置了PyBUF_WRITABLE,否则此函数始终按标志指定的方式填充* view *。

成功后,将view->obj设置为对* exporter *的新引用,并返回 0.否则,引发PyExc_BufferError,将view->obj设置为NULL并返回-1;