緩衝協定 (Buffer Protocol)¶
在 Python 中可使用一些对象来包装对底层内存数组或称 缓冲 的访问。此类对象包括内置的 bytes
和 bytearray
以及一些如 array.array
这样的扩展类型。第三方库也可能会为了特殊的目的而定义它们自己的类型,例如用于图像处理和数值分析等。
虽然这些类型中的每一种都有自己的语义,但它们具有由可能较大的内存缓冲区支持的共同特征。 在某些情况下,希望直接访问该缓冲区而无需中间复制。
Python 以 缓冲协议 的形式在 C 层级上提供这样的功能。 此协议包括两个方面:
在生产者这一方面,该类型的协议可以导出一个“缓冲区接口”,允许公开它的底层缓冲区信息。该接口的描述信息在 缓冲区对象结构体 一节中;
在消费者一侧,有几种方法可用于获得指向对象的原始底层数据的指针(例如一个方法的形参)。
一些简单的对象例如 bytes
和 bytearray
会以面向字节的形式公开它们的底层缓冲区。 也可能会用其他形式;例如 array.array
所公开的元素可以是多字节值。
缓冲区接口的消费者的一个例子是文件对象的 write()
方法:任何可以输出为一系列字节流的对象都可以被写入文件。 然而 write()
只需要对传入对象内容的只读权限,其他的方法如 readinto()
需要对参数内容的写入权限。 缓冲区接口使用对象可以选择性地允许或拒绝读写或只读缓冲区的导出。
对于缓冲区接口的使用者而言,有两种方式来获取一个目的对象的缓冲:
使用正确的参数来调用
PyObject_GetBuffer()
函数;调用
PyArg_ParseTuple()
(或其同级对象之一) 并传入y*
,w*
ors*
格式代码 中的一个。
在这两种情况下,当不再需要缓冲区时必须调用 PyBuffer_Release()
。如果此操作失败,可能会导致各种问题,例如资源泄漏。
缓冲区结构¶
缓冲区结构(或者简单地称为“buffers”)对于将二进制数据从另一个对象公开给 Python 程序员非常有用。它们还可以用作零拷贝切片机制。使用它们引用内存块的能力,可以很容易地将任何数据公开给 Python 程序员。内存可以是 C 扩展中的一个大的常量数组,也可以是在传递到操作系统库之前用于操作的原始内存块,或者可以用来传递本机内存格式的结构化数据。
与 Python 解释器公开的大多部数据类型不同,缓冲区不是 PyObject
指针而是简单的 C 结构。 这使得它们可以非常简单地创建和复制。 当需要为缓冲区加上泛型包装器时,可以创建一个 内存视图 对象。
有关如何编写并导出对象的简短说明,请参阅 缓冲区对象结构。 要获取缓冲区对象,请参阅 PyObject_GetBuffer()
。
-
type Py_buffer¶
- 属于 稳定 ABI (包括所有成员) 自 3.11 版起.
-
void *buf¶
指向由缓冲区字段描述的逻辑结构开始的指针。 这可以是导出程序底层物理内存块中的任何位置。 例如,使用负的
strides
值可能指向内存块的末尾。对于 contiguous ,‘邻接’数组,值指向内存块的开头。
-
PyObject *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_SIMPLE
或PyBUF_WRITABLE
。
-
int readonly¶
缓冲区是否为只读的指示器。此字段由
PyBUF_WRITABLE
标志控制。
-
Py_ssize_t itemsize¶
单个元素的项大小(以字节为单位)。与
struct.calcsize()
调用非NULL
format
的值相同。重要例外:如果使用者请求的缓冲区没有
PyBUF_FORMAT
标志,format
将设置为NULL
,但itemsize
仍具有原始格式的值。如果
shape
存在,则相等的product(shape) * itemsize == len
仍然存在,使用者可以使用itemsize
来导航缓冲区。如果
shape
是NULL
,因为结果为PyBUF_SIMPLE
或PyBUF_WRITABLE
请求,则使用者必须忽略itemsize
,并假设itemsize == 1
。
-
const char *format¶
在
struct
模块样式语法中 NUL 字符串,描述单个项的内容。如果这是NULL
,则假定为"B"
(无符号字节) 。此字段由
PyBUF_FORMAT
标志控制。
-
int ndim¶
内存表示为 n 维数组形式对应的维度数。 如果为
0
,则buf
指向表示标量的单个条目。 在这种情况下,shape
,strides
和suboffsets
必须为NULL
。 最大维度数由PyBUF_MAX_NDIM
给出。
-
Py_ssize_t *shape¶
一个长度为
Py_ssize_t
的数组ndim
表示作为 n 维数组的内存形状。 请注意,shape[0] * ... * shape[ndim-1] * itemsize
必须等于len
。Shape 形状数组中的值被限定在
shape[n] >= 0
。shape[n] == 0
这一情形需要特别注意。更多信息请参阅 complex arrays 。shape 数组对于使用者来说是只读的。
-
Py_ssize_t *strides¶
一个长度为
Py_ssize_t
的数组ndim
给出要跳过的字节数以获取每个尺寸中的新元素。Stride 步幅数组中的值可以为任何整数。对于常规数组,步幅通常为正数,但是使用者必须能够处理
strides[n] <= 0
的情况。更多信息请参阅 complex arrays 。strides数组对用户来说是只读的。
-
Py_ssize_t *suboffsets¶
一个长度为
ndim
类型为Py_ssize_t
的数组 。如果suboffsets[n] >= 0
,则第 n 维存储的是指针,suboffset 值决定了解除引用时要给指针增加多少字节的偏移。suboffset 为负值,则表示不应解除引用(在连续内存块中移动)。如果所有子偏移均为负(即无需取消引用),则此字段必须为
NULL
(默认值)。Python Imaging Library (PIL) 中使用了这种类型的数组表达方式。请参阅 complex arrays 来了解如何从这样一个数组中访问元素。
suboffsets 数组对于使用者来说是只读的。
-
void *internal¶
供输出对象内部使用。比如可能被输出程序重组为一个整数,用于存储一个标志,标明在缓冲区释放时是否必须释放 shape、strides 和 suboffsets 数组。消费者程序 不得 修改该值。
-
void *buf¶
常量:
-
PyBUF_MAX_NDIM¶
内存表示的最大维度数。 导出程序必须遵守这个限制,多维缓冲区的使用者应该能够处理最多
PyBUF_MAX_NDIM
个维度。 目前设置为 64。
缓冲区请求的类型¶
通常,通过 PyObject_GetBuffer()
向输出对象发送缓冲区请求,即可获得缓冲区。由于内存的逻辑结构复杂,可能会有很大差异,缓冲区使用者可用 flags 参数指定其能够处理的缓冲区具体类型。
所有 Py_buffer
字段均由请求类型无歧义地定义。
与请求无关的字段¶
只读,格式¶
PyBUF_WRITABLE
可以和下一节的所有标志联用。由于 PyBUF_SIMPLE
定义为 0,所以 PyBUF_WRITABLE
可以作为一个独立的标志,用于请求一个简单的可写缓冲区。
PyBUF_FORMAT
可以被设为除了 PyBUF_SIMPLE
之外的任何标志。 后者已经按暗示了 B
(无符号字节串)格式。
形状,步幅,子偏移量¶
控制内存逻辑结构的标志按照复杂度的递减顺序列出。注意,每个标志包含它下面的所有标志。
请求 |
形状 |
步幅 |
子偏移量 |
---|---|---|---|
|
是 |
是 |
如果需要的话 |
|
是 |
是 |
NULL |
|
是 |
NULL |
NULL |
|
NULL |
NULL |
NULL |
连续性的请求¶
可以显式地请求C 或 Fortran 连续 ,不管有没有步幅信息。若没有步幅信息,则缓冲区必须是 C-连续的。
请求 |
形状 |
步幅 |
子偏移量 |
邻接 |
---|---|---|---|---|
|
是 |
是 |
NULL |
C |
|
是 |
是 |
NULL |
F |
|
是 |
是 |
NULL |
C 或 F |
是 |
NULL |
NULL |
C |
复合请求¶
所有可能的请求都由上一节中某些标志的组合完全定义。为方便起见,缓冲区协议提供常用的组合作为单个标志。
在下表中,U 代表连续性未定义。消费者程序必须调用 PyBuffer_IsContiguous()
以确定连续性。
请求 |
形状 |
步幅 |
子偏移量 |
邻接 |
readonly |
format |
---|---|---|---|---|---|---|
|
是 |
是 |
如果需要的话 |
U |
0 |
是 |
|
是 |
是 |
如果需要的话 |
U |
1 或 0 |
是 |
|
是 |
是 |
NULL |
U |
0 |
是 |
|
是 |
是 |
NULL |
U |
1 或 0 |
是 |
|
是 |
是 |
NULL |
U |
0 |
NULL |
|
是 |
是 |
NULL |
U |
1 或 0 |
NULL |
|
是 |
NULL |
NULL |
C |
0 |
NULL |
|
是 |
NULL |
NULL |
C |
1 或 0 |
NULL |
复杂数组¶
NumPy-风格:形状和步幅¶
NumPy 风格数组的逻辑结构由 itemsize
、 ndim
、 shape
和 strides
定义。
如果 ndim == 0
, buf
指向的内存位置被解释为大小为 itemsize
的标量。这时, shape
和 strides
都为 NULL
。
如果 strides
为 NULL
,则数组将被解释为一个标准的 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]
数组。
这是一个函数,当n维索引所指向的N-D数组中有 NULL
步长和子偏移量时,它返回一个指针
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;
}