缓冲区协议¶
Python 中某些对象封装了对底层内存数组或 **缓冲器** 的访问。这些对象包括内置的 bytes
和 bytearray
,以及一些扩展类型,如 array.array
。第三方库可能会为图像处理或数值分析等特殊目的定义自己的类型。
虽然这些类型都有各自的语义,但它们都具有由可能较大的内存缓冲区支持的共同特征。因此,在某些情况下,希望直接访问该缓冲区而无需中间复制。
Python 在 C 和 Python 级别通过 缓冲区协议 提供了这种机制。该协议分为两方面:
在生产者方面,类型可以导出一个“缓冲区接口”,允许该类型的对象公开有关其底层缓冲区的信息。此接口在 缓冲区对象结构 部分中描述;对于 Python,请参阅 模拟缓冲区类型。
在消费者方面,有多种方法可以获取指向对象原始底层数据(例如方法参数)的指针。对于 Python,请参阅
memoryview
。
像 bytes
和 bytearray
这样的简单对象以字节形式公开其底层缓冲区。其他形式也是可能的;例如,array.array
公开的元素可以是多字节值。
缓冲区接口的一个消费者示例是文件对象的 write()
方法:任何可以通过缓冲区接口导出字节序列的对象都可以写入文件。虽然 write()
只需要对其传入对象的内部内容进行只读访问,但其他方法(如 readinto()
)需要对其参数的内容进行写入访问。缓冲区接口允许对象选择性地允许或拒绝导出读写和只读缓冲区。
缓冲区接口的消费者有两种方式获取目标对象的缓冲区:
调用具有正确参数的
PyObject_GetBuffer()
;调用
PyArg_ParseTuple()
(或其姊妹函数之一),并使用y*
、w*
或s*
中的一个 格式代码。
在这两种情况下,当不再需要缓冲区时,必须调用 PyBuffer_Release()
。否则可能会导致各种问题,例如资源泄漏。
3.12 版本新增:缓冲区协议现在可在 Python 中访问,请参阅 模拟缓冲区类型 和 memoryview
。
缓冲区结构¶
缓冲区结构(或简称“缓冲区”)是一种将其他对象的二进制数据公开给 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_SIMPLE
或PyBUF_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_SIMPLE
或PyBUF_WRITABLE
请求导致shape
为NULL
,则消费者必须忽略itemsize
并假定itemsize == 1
。
-
char *format¶
一个以
struct
模块样式语法描述单个项内容的 *NULL* 终止字符串。如果为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[n] >= 0
。当shape[n] == 0
时需要特别注意。有关更多信息,请参阅 复杂数组。形状数组对消费者来说是只读的。
-
Py_ssize_t *strides¶
一个长度为
Py_ssize_t
的数组,其长度为ndim
,给出在每个维度中跳过多少字节才能到达新元素。步幅值可以是任何整数。对于常规数组,步幅通常为正,但消费者必须能够处理
strides[n] <= 0
的情况。有关更多信息,请参阅 复杂数组。步幅数组对消费者来说是只读的。
-
Py_ssize_t *suboffsets¶
一个长度为
Py_ssize_t
的数组,其长度为ndim
。如果suboffsets[n] >= 0
,则沿第 n 维存储的值是指针,并且子偏移量值指示在解引用后要添加到每个指针的字节数。负的子偏移量值表示不应发生解引用(在连续内存块中跨步)。如果所有子偏移量都为负(即不需要解引用),则此字段必须为
NULL
(默认值)。这种数组表示形式由 Python 图像库 (PIL) 使用。有关如何访问此类数组元素的更多信息,请参阅 复杂数组。
子偏移量数组对消费者来说是只读的。
-
void *internal¶
这供导出对象内部使用。例如,导出器可能会将其重新强制转换为整数,并用于存储有关在释放缓冲区时是否必须释放形状、步幅和子偏移量数组的标志。消费者不得更改此值。
-
void *buf¶
常量
-
PyBUF_MAX_NDIM¶
内存表示的最大维度数。导出器必须遵守此限制,多维缓冲区的消费者应能处理多达
PyBUF_MAX_NDIM
维。当前设置为 64。
缓冲区请求类型¶
通常通过 PyObject_GetBuffer()
向导出对象发送缓冲区请求来获取缓冲区。由于内存逻辑结构的复杂性可能大相径庭,消费者使用 *flags* 参数来指定它可以处理的确切缓冲区类型。
所有 Py_buffer
字段都由请求类型明确定义。
请求无关字段¶
只读,格式¶
- PyBUF_WRITABLE¶
控制
readonly
字段。如果设置,导出器必须提供可写缓冲区,否则报告失败。否则,导出器可以提供只读或可写缓冲区,但选择必须对所有消费者一致。例如,PyBUF_SIMPLE | PyBUF_WRITABLE 可用于请求简单的可写缓冲区。
PyBUF_WRITABLE
可以与下一节中的任何标志进行或运算。由于 PyBUF_SIMPLE
定义为 0,因此 PyBUF_WRITABLE
可以作为独立标志来请求简单的可写缓冲区。
PyBUF_FORMAT
必须与除 PyBUF_SIMPLE
之外的任何标志进行或运算,因为后者已经隐含了格式 B
(无符号字节)。PyBUF_FORMAT
不能单独使用。
形状、步幅、子偏移量¶
控制内存逻辑结构的标志按复杂性递减的顺序排列。请注意,每个标志都包含其下方所有标志的所有位。
请求 |
形状 |
步幅 |
子偏移量 |
---|---|---|---|
|
是 |
是 |
如果需要 |
|
是 |
是 |
NULL |
|
是 |
NULL |
NULL |
|
NULL |
NULL |
NULL |
连续性请求¶
可以明确请求 C 或 Fortran 连续性,带或不带步幅信息。不带步幅信息时,缓冲区必须是 C 连续的。
请求 |
形状 |
步幅 |
子偏移量 |
连续性 |
---|---|---|---|---|
|
是 |
是 |
NULL |
C |
|
是 |
是 |
NULL |
F |
|
是 |
是 |
NULL |
C 或 F |
是 |
NULL |
NULL |
C |
复合请求¶
所有可能的请求都由上一节中的标志的某些组合完全定义。为方便起见,缓冲区协议提供了常用组合作为单个标志。
在下表中,*U* 代表未定义的连续性。消费者需要调用 PyBuffer_IsContiguous()
来确定连续性。
请求 |
形状 |
步幅 |
子偏移量 |
连续性 |
只读 |
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]
数组。
以下是一个函数,当步幅和子偏移量都非 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;
}