缓冲协议

在 Python 中可使用一些对象来包装对底层内存数组或称 缓冲 的访问。此类对象包括内置的 bytesbytearray 以及一些如 array.array 这样的扩展类型。第三方库也可能会为了特殊的目的而定义它们自己的类型,例如用于图像处理和数值分析等。

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

Python 以 缓冲协议 的形式在 C 层级上提供这样的功能。 此协议包括两个方面:

  • 在生产者这一方面,该类型的协议可以导出一个“缓冲区接口”,允许公开它的底层缓冲区信息。该接口的描述信息在 缓冲区对象结构体 一节中;

  • 在消费者一侧,有几种方法可用于获得指向对象的原始底层数据的指针(例如一个方法的形参)。

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

缓冲区接口的消费者的一个例子是文件对象的 write() 方法:任何可以输出为一系列字节流的对象都可以被写入文件。 然而 write() 只需要对传入对象内容的只读权限,其他的方法如 readinto() 需要对参数内容的写入权限。 缓冲区接口使用对象可以选择性地允许或拒绝读写或只读缓冲区的导出。

对于缓冲区接口的使用者而言,有两种方式来获取一个目的对象的缓冲:

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

缓冲区结构

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

与 Python 解释器公开的大多部数据类型不同,缓冲区不是 PyObject 指针而是简单的 C 结构。 这使得它们可以非常简单地创建和复制。 当需要为缓冲区加上泛型包装器时,可以创建一个 内存视图 对象。

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

  • type Py_buffer
  • 属于 稳定 ABI (包括所有成员) 自 3.11 版起.
    • void *buf
  • 指向由缓冲区字段描述的逻辑结构开始的指针。 这可以是导出程序底层物理内存块中的任何位置。 例如,使用负的 strides 值可能指向内存块的末尾。

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

  • 对导出对象的新引用。 该引用由消费方拥有,并由 PyBuffer_Release() 自动释放(即引用计数递减)并设置为 NULL。 该字段相当于任何标准 C-API 函数的返回值。

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

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

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

  • int readonly

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

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

如果 shapeNULL,因为结果为 PyBUF_SIMPLEPyBUF_WRITABLE 请求,则使用者必须忽略 itemsize,并假设 itemsize == 1

  • char *format
  • struct 模块样式语法中以 NULL 结束的字符串描述单个条目的内容。 如果这是 NULL,将假定为 "B" (无符号字节型)。

此字段由 PyBUF_FORMAT 标志控制。

  • int ndim
  • 内存表示为 n 维数组形式对应的维度数。 如果为 0,则 buf 指向表示标量的单个条目。 在这种情况下,shape, stridessuboffsets 必须为 NULL。 最大维度数由 PyBUF_MAX_NDIM 给出。

  • Py_ssize_t *shape

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

Shape 形状数组中的值被限定在 shape[n] >= 0shape[n] == 0 这一情形需要特别注意。更多信息请参阅 complex arrays

shape 数组对于使用者来说是只读的。

  • 一个长度为 Py_ssize_t 的数组 ndim 给出要跳过的字节数以获取每个尺寸中的新元素。

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

strides数组对用户来说是只读的。

  • 一个长度为 ndim 类型为 Py_ssize_t 的数组 。如果 suboffsets[n] >= 0,则第 n 维存储的是指针,suboffset 值决定了解除引用时要给指针增加多少字节的偏移。suboffset 为负值,则表示不应解除引用(在连续内存块中移动)。

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

Python Imaging Library (PIL) 中使用了这种类型的数组表达方式。请参阅 complex arrays 来了解如何从这样一个数组中访问元素。

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

  • void *internal
  • 供输出对象内部使用。比如可能被输出程序重组为一个整数,用于存储一个标志,标明在缓冲区释放时是否必须释放 shape、strides 和 suboffsets 数组。消费者程序 不得 修改该值。

常量:

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

缓冲区请求的类型

通常,通过 PyObject_GetBuffer() 向输出对象发送缓冲区请求,即可获得缓冲区。由于内存的逻辑结构复杂,可能会有很大差异,缓冲区使用者可用 flags 参数指定其能够处理的缓冲区具体类型。

所有 Py_buffer 字段均由请求类型无歧义地定义。

与请求无关的字段

以下字段不会被 flags 影响,并且必须总是用正确的值填充:obj, buflenitemsizendim

只读,格式

PyBUF_WRITABLE

控制 readonly 字段。 如果设置了,输出程序必须提供一个可写的缓冲区,否则将报告失败。 在其他情况下,输出程序可以提供一个只读或可写的缓冲区,但该选择必须对所有消费者程序保持一致。 例如,可以使用 PyBUF_SIMPLE | PyBUF_WRITABLE 来请求一个简单的可写缓冲区。 be used to request a simple writable buffer.

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() 以确定连续性。

请求 形状 步幅 子偏移量 邻接 只读 format
- 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 == 0buf 指向的内存位置被解释为大小为 itemsize 的标量。这时, shapestrides 都为 NULL

如果 stridesNULL,则数组将被解释为一个标准的 n 维 C 语言数组。否则,消费者程序必须按如下方式访问 n 维数组:

  1. ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1];
  2. item = *((typeof(item) *)ptr);

如上所述,buf 可以指向实际内存块中的任意位置。输出者程序可以用该函数检查缓冲区的有效性。

  1. def verify_structure(memlen, itemsize, ndim, shape, strides, offset): """验证形参代表已分配内存范围内一个可用的数组:
  2. char *mem: 物理内存块的起始
  3. memlen: 物理内存块的长度
  4. offset: (char *)buf - mem
  5. """
  6. if offset % itemsize:
  7. return False
  8. if offset < 0 or offset+itemsize > memlen:
  9. return False
  10. if any(v % itemsize for v in strides):
  11. return False
  12.  
  13. if ndim <= 0:
  14. return ndim == 0 and not shape and not strides
  15. if 0 in shape:
  16. return True
  17.  
  18. imin = sum(strides[j]*(shape[j]-1) for j in range(ndim)
  19. if strides[j] <= 0)
  20. imax = sum(strides[j]*(shape[j]-1) for j in range(ndim)
  21. if strides[j] > 0)
  22.  
  23. 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] 数组。

这是一个函数,当n维索引所指向的N-D数组中有 NULL 步长和子偏移量时,它返回一个指针

  1. 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;
  2. }

缓冲区相关函数

  • int PyObject_CheckBuffer(PyObject *obj)
  • 属于 稳定 ABI 自 3.11 版起. 如果 obj 支持缓冲区接口,则返回 1,否则返回 0。 返回 1 时不保证 PyObject_GetBuffer() 一定成功。本函数一定调用成功。
  • 属于 稳定 ABI 自 3.11 版起. 向 exporter 发送请求以按照 flags 指定的内容填充 view。 如果 exporter 无法提供要求类型的缓冲区,则它必须引发 BufferError,将 view->obj 设为 NULL 并返回 -1

成功时,填充 view,将 view->obj 设为对 exporter 的新引用,并返回 0。 当链式缓冲区提供程序将请求重定向到一个对象时,view->obj 可以引用该对象而不是 exporter (参见 缓冲区对象结构)。

PyObject_GetBuffer() 必须与 PyBuffer_Release() 同时调用成功,类似于 malloc()free()。因此,消费者程序用完缓冲区后, PyBuffer_Release() 必须保证被调用一次。

  • 属于 稳定 ABI 自 3.11 版起. 释放缓冲区 view 并释放对视图的支持对象 view->objstrong reference (即递减引用计数)。 该函数必须在缓冲区不再使用时调用,否则可能会发生引用泄漏。

若该函数针对的缓冲区不是通过 PyObject_GetBuffer() 获得的,将会出错。

  • Py_ssize_t PyBuffer_SizeFromFormat(const char *format)
  • 属于 稳定 ABI 自 3.11 版起. 从 format 返回隐含的 itemsize。 如果出错,则引发异常并返回 -1。

Added in version 3.9.

  • int PyBuffer_IsContiguous(const Py_buffer *view, char order)
  • 属于 稳定 ABI 自 3.11 版起. 如果 view 定义的内存是 C 风格(order 为 'C')或 Fortran 风格(order 为 'F'contiguous 或其中之一(order 是 'A'),则返回 1。否则返回 0。该函数总会成功。
  • 属于 稳定 ABI 自 3.11 版起. 获取给定 view 内的 indices 所指向的内存区域。indices 必须指向一个 view->ndim 索引的数组。
  • 属于 稳定 ABI 自 3.11 版起. 从 buf 复制连续的 len 字节到 view 。fort 可以是 'C''F' (对应于 C 风格或 Fortran 风格的顺序)。成功时返回 0 ,错误时返回 -1
  • 属于 稳定 ABI 自 3.11 版起. 从 src 复制 len 字节到 buf ,成为连续字节串的形式。order 可以是 'C''F''A' (对应于 C 风格、Fortran 风格的顺序或其中任意一种)。成功时返回 0 ,出错时返回 -1

如果 len != src->len 则此函数将报错。

  • 属于 稳定 ABI 自 3.11 版起. 将数据从 src 拷贝到 dest 缓冲区。 可以在 C 风格或 Fortran 风格的缓冲区之间进行转换。

成功时返回 0,出错时返回 -1

  • void PyBuffer_FillContiguousStrides(int ndims, Py_ssize_t shape, Py_ssize_t strides, int itemsize, char order)
  • 属于 稳定 ABI 自 3.11 版起. 用给定形状的 contiguous 字节串数组 (如果 order 为 'C' 则为 C 风格,如果 order 为 'F' 则为 Fortran 风格) 来填充 strides 数组,每个元素具有给定的字节数。
  • 属于 稳定 ABI 自 3.11 版起. 处理导出程序的缓冲区请求,该导出程序要公开大小为 len 的 buf ,并根据 readonly 设置可写性。bug 被解释为一个无符号字节序列。

参数 flags 表示请求的类型。该函数总是按照 flag 指定的内容填入 view,除非 buf 设为只读,并且 flag 中设置了 PyBUF_WRITABLE 标志。

成功时,将 view->obj 设为对 exporter 的新引用并返回 0。 否则,引发 BufferError,将 view->obj 设为 NULL 并返回 -1

如果此函数用作 getbufferproc 的一部分,则 exporter 必须设置为导出对象,并且必须在未修改的情况下传递 flags。否则,exporter 必须是 NULL