缓冲协议 — Python 文档
缓冲协议
Python 中可用的某些对象包装对底层内存数组或 缓冲区 的访问。 此类对象包括内置的 bytes 和 bytearray,以及一些扩展类型,如 array.array。 第三方库可能会出于特殊目的定义自己的类型,例如图像处理或数值分析。
虽然这些类型中的每一种都有自己的语义,但它们都有一个共同的特征,即由可能很大的内存缓冲区支持。 在某些情况下,需要直接访问该缓冲区,而无需进行中间复制。
Python 以 缓冲区协议 的形式在 C 级别提供了这样的功能。 该协议有两个方面:
- 在生产者方面,一个类型可以导出一个“缓冲区接口”,它允许该类型的对象公开有关其底层缓冲区的信息。 这个接口在Buffer Object Structures部分有描述;
- 在消费者方面,有几种方法可以获得指向对象原始底层数据的指针(例如方法参数)。
bytes 和 bytearray 等简单对象以面向字节的形式公开其底层缓冲区。 其他形式也是可能的; 例如,array.array 公开的元素可以是多字节值。
缓冲区接口的一个示例消费者是文件对象的 write() 方法:任何可以通过缓冲区接口导出一系列字节的对象都可以写入文件。 write()
只需要对传递给它的对象的内部内容进行只读访问,而其他方法(如 readinto())则需要对其参数的内容进行写访问。 缓冲区接口允许对象有选择地允许或拒绝读写和只读缓冲区的导出。
缓冲区接口的使用者有两种方法可以通过目标对象获取缓冲区:
- 使用正确的参数调用 PyObject_GetBuffer();
- 使用
y*
、w*
或s*
格式代码 之一调用 PyArg_ParseTuple()(或其同级之一)。
在这两种情况下,当不再需要缓冲区时,必须调用 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_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 来导航缓冲区。如果
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
将最大维度数限制为 64。 出口商必须遵守这个限制,多维缓冲区的消费者应该能够处理最多PyBUF_MAX_NDIM
维。
- Py_ssize_t \*shape
长度为 ndim 的
Py_ssize_t
数组表示作为 n 维数组的内存形状。 请注意,shape[0] * ... * shape[ndim-1] * itemsize
必须等于 len。形状值限制为
shape[n] >= 0
。 外壳shape[n] == 0
需要特别注意。 有关更多信息,请参阅 复杂数组 。形状数组对使用者是只读的。
- Py_ssize_t \*strides
长度为 ndim 的
Py_ssize_t
数组给出了要跳过以到达每个维度中的新元素的字节数。步幅值可以是任何整数。 对于常规数组,步幅通常是正数,但消费者必须能够处理这种情况
strides[n] <= 0
。 有关更多信息,请参阅 复杂数组 。strides 数组对于消费者是只读的。
- Py_ssize_t \*suboffsets
长度为 ndim 的
Py_ssize_t
数组。 如果suboffsets[n] >= 0
,沿第 n 维存储的值是指针,并且子偏移值指示在取消引用后要添加到每个指针的字节数。 为负的 suboffset 值表示不应发生取消引用(跨连续内存块)。如果所有的子偏移都是负的(即 不需要取消引用),则该字段必须为
NULL
(默认值)。这种类型的数组表示由 Python 图像库 (PIL) 使用。 有关如何访问此类数组元素的更多信息,请参阅 复杂数组 。
suboffsets 数组对于使用者是只读的。
- void \*internal
这是供导出对象在内部使用。 例如,这可能会被导出器重新转换为整数,并用于存储有关在释放缓冲区时是否必须释放形状、步幅和子偏移量数组的标志。 消费者不得更改此值。
缓冲区请求类型
缓冲区通常是通过 PyObject_GetBuffer() 向导出对象发送缓冲区请求来获得的。 由于内存逻辑结构的复杂性可能会有很大差异,消费者使用 flags 参数来指定它可以处理的确切缓冲区类型。
所有 Py_buffer 字段都由请求类型明确定义。
只读,格式
- PyBUF_WRITABLE
- 控制 readonly 字段。 如果设置,导出器必须提供可写缓冲区,否则报告失败。 否则,出口商可以提供只读或可写缓冲区,但选择必须对所有消费者保持一致。
- PyBUF_FORMAT
- 控制
format
字段。 如果设置,则必须正确填写此字段。 否则,该字段必须为NULL
。
PyBUF_WRITABLE 可以|'d 到下一节中的任何标志。 由于 PyBUF_SIMPLE 定义为 0,因此 PyBUF_WRITABLE 可以用作独立标志来请求简单的可写缓冲区。
PyBUF_FORMAT 可以被 |'d 到除 PyBUF_SIMPLE 之外的任何标志。 后者已经暗示了格式 B
(无符号字节)。
形状、步幅、子偏移
控制存储器逻辑结构的标志按复杂性降序列出。 请注意,每个标志都包含其下方标志的所有位。
要求 | 形状 | 大步 | 子偏移量 |
---|---|---|---|
|
是的 | 是的 | 如果需要的话 |
|
是的 | 是的 | 无效的 |
|
是的 | 无效的 | 无效的 |
|
无效的 | 无效的 | 无效的 |
邻接请求
C 或 Fortran 连续性 可以明确请求,有和没有步幅信息。 如果没有步幅信息,缓冲区必须是 C 连续的。
要求 | 形状 | 大步 | 子偏移量 | 重叠群 |
---|---|---|---|---|
|
是的 | 是的 | 无效的 | C |
|
是的 | 是的 | 无效的 | F |
|
是的 | 是的 | 无效的 | C 或 F |
|
是的 | 无效的 | 无效的 | C |
复合请求
所有可能的请求都由上一节中的标志的某种组合完全定义。 为方便起见,缓冲协议提供了常用的组合作为单个标志。
在下表中 U 代表未定义的连续性。 消费者必须调用 PyBuffer_IsContiguous() 来确定连续性。
要求 | 形状 | 大步 | 子偏移量 | 重叠群 | 只读 | 格式 |
---|---|---|---|---|---|---|
|
是的 | 是的 | 如果需要的话 | U | 0 | 是的 |
|
是的 | 是的 | 如果需要的话 | U | 1 或 0 | 是的 |
|
是的 | 是的 | 无效的 | U | 0 | 是的 |
|
是的 | 是的 | 无效的 | U | 1 或 0 | 是的 |
|
是的 | 是的 | 无效的 | U | 0 | 无效的 |
|
是的 | 是的 | 无效的 | U | 1 或 0 | 无效的 |
|
是的 | 无效的 | 无效的 | C | 0 | 无效的 |
|
是的 | 无效的 | 无效的 | C | 1 或 0 | 无效的 |
复杂数组
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 维索引指向的 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;
}