PEP 3118 – 修订缓冲协议
- 作者:
- Travis Oliphant <oliphant at ee.byu.edu>, Carl Banks <pythondev at aerojockey.com>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2006年8月28日
- Python版本:
- 3.0
- 发布历史:
摘要
本PEP建议重新设计缓冲区接口(PyBufferProcs
函数指针),以改进Python在Python 3.0中允许内存共享的方式。
特别是,建议取消API的字符缓冲区部分,并重新设计多段部分,同时允许共享步长内存。此外,新的缓冲区接口将允许共享内存的任何多维特性以及内存包含的数据格式。
此接口将允许任何扩展模块创建共享内存的对象或创建使用和操作来自导出接口的任意对象的原始内存的算法。
基本原理
Python 2.X缓冲区协议允许不同的Python类型交换指向内部缓冲区序列的指针。此功能对于在不同的高级对象之间共享大块内存非常有用,但它过于有限并且存在问题。
- 存在很少使用的“段序列”选项(bf_getsegcount),其动机不明确。
- 存在显然冗余的字符缓冲区选项(bf_getcharbuffer)。
- 使用者无法告诉导出缓冲区API的对象它已“完成”对内存的查看,因此导出对象无法确定重新分配其拥有的内存指针是否安全(例如,数组对象在与持有原始指针的缓冲区对象共享内存后重新分配其内存导致臭名昭著的缓冲区对象问题)。
- 内存只是一个带有长度的指针。无法描述内存中“包含”什么(浮点数、整数、C结构体等)。
- 没有提供内存的形状信息。但是,几个类似数组的Python类型可以利用标准方法来描述内存的形状解释(wxPython、GTK、pyQT、CVXOPT、PyVox、音频和视频库、ctypes、NumPy、数据库接口等)。
- 无法共享不连续内存(除非通过段序列的概念)。
有两个广泛使用的库使用了不连续内存的概念:PIL和NumPy。尽管如此,它们对不连续数组的看法是不同的。提议的缓冲区接口允许共享任一内存模型。导出器通常只使用一种方法,使用者可以根据自己的选择选择支持每种类型的不连续数组。
NumPy使用每个维度恒定步长的概念作为其数组的基本概念。有了这个概念,可以描述更大数组的简单子区域而无需复制数据。因此,步长信息是必须共享的附加信息。
PIL使用更不透明的内存表示。有时图像包含在连续的内存段中,但有时它包含在指向图像连续段(通常是行)的指针数组中。PIL是原始缓冲区接口中多个缓冲区段的想法来源。
NumPy的步长内存模型在计算库中更常使用,并且由于它非常简单,因此使用此模型支持内存共享是有意义的。PIL内存模型有时用于C代码中,其中可以使用双指针间接寻址访问二维数组:例如
image[i][j]
。缓冲区接口应允许对象导出这些内存模型中的任何一个。使用者可以自由地要求连续内存或编写代码来处理这些内存模型中的一个或两个。
提案概述
- 消除缓冲区协议的字符缓冲区和多段部分。
- 统一获取缓冲区的读/写版本。
- 向接口添加一个新函数,当使用者对象“完成”对内存区域的操作时应该调用该函数。
- 添加一个新变量,以允许接口描述内存中包含的内容(统一当前在struct和array中完成的操作)。
- 添加一个新变量,以允许协议共享形状信息。
- 添加一个新变量用于共享步长信息。
- 添加一种新机制用于共享必须使用指针间接寻址访问的数组。
- 修复核心和标准库中的所有对象以符合新接口。
- 扩展struct模块以处理更多格式说明符。
- 将缓冲区对象扩展到一个新的内存对象,该对象在缓冲区接口周围放置一个Python外壳。
- 添加一些函数,以便轻松地将连续数据复制进出支持缓冲区接口的对象。
规范
虽然新的规范允许复杂的内存共享,但仍然可以从对象中获取简单的连续字节缓冲区。事实上,即使原始对象不是表示为连续的内存块,新协议也允许使用标准机制来执行此操作。
获取简单连续内存块的最简单方法是使用提供的C-API获取一块内存。
将PyBufferProcs
结构更改为
typedef struct {
getbufferproc bf_getbuffer;
releasebufferproc bf_releasebuffer;
} PyBufferProcs;
这两个例程对于类型对象都是可选的。
typedef int (*getbufferproc)(PyObject *obj, PyBuffer *view, int flags)
此函数在成功时返回0
,在失败时返回-1
(并引发错误)。第一个变量是“导出”对象。第二个参数是缓冲区信息结构体的地址。这两个参数都绝不能为NULL。
第三个参数指示使用者准备处理哪种缓冲区,因此也指示导出器允许返回哪种缓冲区。新的缓冲区接口允许更复杂的内存共享可能性。某些使用者可能无法处理所有复杂性,但可能希望查看导出器是否允许他们对其内存进行更简单的查看。
此外,一些导出器可能无法以任何可能的方式共享内存,并且可能需要引发错误以向某些使用者发出信号,表明某些事情根本不可能。除非存在导致问题的其他错误,否则这些错误应为PyErr_BufferError
。导出器可以使用标志信息简化PyBuffer结构中填充非默认值的元素数量和/或引发错误,如果对象无法支持其内存的更简单的视图。
导出器应始终填充缓冲区结构的所有元素(如果未请求其他内容,则使用默认值或NULL)。对于简单情况,可以使用PyBuffer_FillInfo函数。
访问标志
一些标志对于请求特定类型的内存段很有用,而其他标志则指示导出器使用者可以处理哪种信息。如果使用者没有请求某些信息,但导出器无法在没有该信息的情况下共享其内存,则应引发PyErr_BufferError
。
PyBUF_SIMPLE
这是默认标志状态(0)。返回的缓冲区可能具有可写内存,也可能不具有可写内存。格式将假定为无符号字节。这是一个“独立”的标志常量。它永远不需要与其他标志进行|操作。如果导出器无法提供此类连续的字节缓冲区,则会引发错误。
PyBUF_WRITABLE
返回的缓冲区必须可写。如果不可写,则引发错误。
PyBUF_FORMAT
如果提供了此标志,则返回的缓冲区必须具有真实的格式信息。这将在使用者将要检查实际存储的“类型”数据时使用。导出器应始终能够在请求时提供此信息。如果未显式请求格式,则格式必须返回为NULL
(这意味着“B”,或无符号字节)。
PyBUF_ND
返回的缓冲区必须提供形状信息。内存将假定为C风格连续(最后一个维度变化最快)。如果导出器无法提供这种连续的缓冲区,则可能会引发错误。如果未给出此标志,则形状将为NULL。
PyBUF_STRIDES
(暗示PyBUF_ND
)
返回的缓冲区必须提供步长信息(即步长不能为NULL)。这将在使用者可以处理步长不连续数组时使用。处理步长自动假设您可以处理形状。如果导出器无法提供数据的仅步长表示(即没有子偏移量),则可能会引发错误。
PyBUF_C_CONTIGUOUS
PyBUF_F_CONTIGUOUS
PyBUF_ANY_CONTIGUOUS
这些标志分别指示返回的缓冲区必须是C连续(最后一个维度变化最快)、Fortran连续(第一个维度变化最快)或两者之一。所有这些标志都暗示PyBUF_STRIDES并保证步长缓冲区信息结构将被正确填充。
PyBUF_INDIRECT
(暗示PyBUF_STRIDES
)
返回的缓冲区必须具有子偏移量信息(如果不需要子偏移量,则可以为NULL)。这将在使用者可以处理这些子偏移量隐含的间接数组引用时使用。
特定类型内存共享的标志的专门组合。
多维(但连续)PyBUF_CONTIG
(PyBUF_ND | PyBUF_WRITABLE
)PyBUF_CONTIG_RO
(PyBUF_ND
)使用步长但对齐的多维
PyBUF_STRIDED
(PyBUF_STRIDES | PyBUF_WRITABLE
)PyBUF_STRIDED_RO
(PyBUF_STRIDES
)使用步长且不一定对齐的多维
PyBUF_RECORDS
(PyBUF_STRIDES | PyBUF_WRITABLE | PyBUF_FORMAT
)PyBUF_RECORDS_RO
(PyBUF_STRIDES | PyBUF_FORMAT
)使用子偏移的多维数组
PyBUF_FULL
(PyBUF_INDIRECT | PyBUF_WRITABLE | PyBUF_FORMAT
)PyBUF_FULL_RO
(PyBUF_INDIRECT | PyBUF_FORMAT
)
因此,只需要从对象获取连续字节块的使用者可以使用 PyBUF_SIMPLE
,而能够处理最复杂情况的使用者可以使用 PyBUF_FULL
。
只有在标志参数中包含 PyBUF_FORMAT
时,才能保证格式信息非空,否则使用者应该假设为无符号字节。
有一个 C API,简单的导出对象可以使用它来根据提供的标志正确填充缓冲区信息结构,如果所有可以导出的内容只是一个连续的“无符号字节”块。
Py_buffer结构体
bufferinfo 结构如下所示:
struct bufferinfo {
void *buf;
Py_ssize_t len;
int readonly;
const char *format;
int ndim;
Py_ssize_t *shape;
Py_ssize_t *strides;
Py_ssize_t *suboffsets;
Py_ssize_t itemsize;
void *internal;
} Py_buffer;
在调用 bf_getbuffer 函数之前,可以将 bufferinfo 结构填充任何内容,但在请求新缓冲区时,buf
字段必须为 NULL。从 bf_getbuffer 返回后,bufferinfo 结构将填充与缓冲区相关的相关信息。当使用者完成对内存的操作时,必须将同一个 bufferinfo 结构传递给 bf_releasebuffer(如果可用)。调用者负责在调用 releasebuffer 之前保持对 obj 的引用(即,对 bf_getbuffer 的调用不会更改 obj 的引用计数)。
bufferinfo 结构的成员如下:
buf
- 指向对象内存起始位置的指针。
len
- 对象使用的内存总字节数。这应该与形状数组的乘积乘以每个内存项的字节数相同。
readonly
- 一个整型变量,用于保存内存是否为只读。1 表示内存为只读,0 表示内存可写。
format
- 一个以 NULL 结尾的格式字符串(遵循结构体风格的语法,包括扩展),指示内存中每个元素的内容。元素数量为 len / itemsize,其中 itemsize 是格式隐含的字节数。这可以为 NULL,表示标准无符号字节(“B”)。
ndim
- 一个变量,存储内存表示的维度数。必须大于等于 0。值为 0 表示 shape、strides 和 suboffsets 必须为
NULL
(即,内存表示一个标量)。 shape
- 一个长度为
ndims
的Py_ssize_t
数组,指示内存作为 N 维数组的形状。请注意,((*shape)[0] * ... * (*shape)[ndims-1])*itemsize = len
。如果 ndims 为 0(表示标量),则此值必须为NULL
。 strides
- 一个
Py_ssize_t*
变量的地址,它将填充一个指向长度为ndims
的Py_ssize_t
数组的指针(如果ndims
为 0,则为NULL
)。指示跳过多少字节才能到达每个维度中的下一个元素。如果调用者没有请求此信息(PyBUF_STRIDES
未设置),则应将其设置为 NULL,表示 C 样式的连续数组,或者如果无法实现则引发 PyExc_BufferError。 suboffsets
- 一个
Py_ssize_t *
变量的地址,它将填充一个指向长度为*ndims
的Py_ssize_t
数组的指针。如果这些子偏移数字大于等于 0,则沿指示维度存储的值是一个指针,子偏移值指示在取消引用后向指针添加多少字节。负的子偏移值表示不应进行取消引用(在连续内存块中跨步)。如果所有子偏移都为负(即,不需要取消引用),则此值必须为 NULL(默认值)。如果调用者没有请求此信息(PyBUF_INDIRECT 未设置),则应将其设置为 NULL,或者如果无法实现则引发 PyExc_BufferError。为了清楚起见,以下是一个函数,当存在非空 strides 和 suboffsets 时,它返回指向 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; }
请注意,子偏移是在取消引用“之后”添加的。因此,在第 i 维进行切片将在第 (i-1) 维添加子偏移。在第一维进行切片将直接更改起始指针的位置(即,buf 将被修改)。
itemsize
- 这是共享内存中每个元素的 itemsize(以字节为单位)的存储位置。从技术上讲,这是不必要的,因为它可以使用
PyBuffer_SizeFromFormat
获取,但是导出器可能在不解析格式字符串的情况下就知道此信息,并且需要知道 itemsize 才能正确解释跨步。因此,存储它更方便且更快。 internal
- 供导出对象内部使用。例如,导出器可以将其重新转换为整数,并用于存储有关在释放缓冲区时是否必须释放 shape、strides 和 suboffsets 数组的标志。使用者永远不应该更改此值。
导出器负责确保 buf、format、shape、strides 和 suboffsets 指向的任何内存在调用 releasebuffer 之前都是有效的。如果导出器希望能够在调用 releasebuffer 之前更改对象的 shape、strides 和/或 suboffsets,则应在调用 getbuffer 时分配这些数组(在提供的 buffer-info 结构中指向它们),并在调用 releasebuffer 时释放它们。
释放缓冲区
在 release-buffer 接口调用中应使用相同的 bufferinfo 结构。调用者负责 Py_buffer 结构本身的内存。
typedef void (*releasebufferproc)(PyObject *obj, Py_buffer *view)
getbufferproc 的调用者必须确保在不再需要从对象获取的内存时调用此函数。接口的导出者必须确保 bufferinfo 结构中指向的任何内存在调用 releasebuffer 之前保持有效。
如果没有提供 bf_releasebuffer 函数(即它为 NULL),则永远不需要调用它。
如果导出器可以重新分配其内存、strides、shape、suboffsets 或 format 变量(它们可能通过 struct bufferinfo 共享),则需要定义一个 bf_releasebuffer 函数。可以使用几种机制来跟踪已进行了多少次 getbuffer 调用以及共享了多少内容。可以使用单个变量来跟踪导出了多少个“视图”,或者可以在每个对象中维护一个填充了 bufferinfo 结构的链表。
但是,导出器唯一明确需要做的事情是,确保通过 bufferinfo 结构共享的任何内存在对导出该内存的 bufferinfo 结构调用 releasebuffer 之前保持有效。
提出新的C-API调用
int PyObject_CheckBuffer(PyObject *obj)
如果 getbuffer 函数可用则返回 1,否则返回 0。
int PyObject_GetBuffer(PyObject *obj, Py_buffer *view,
int flags)
这是 getbuffer 函数调用的 C API 版本。它检查对象是否具有所需的函数指针并发出调用。如果失败则返回 -1 并引发错误,如果成功则返回 0。
void PyBuffer_Release(PyObject *obj, Py_buffer *view)
这是 releasebuffer 函数调用的 C API 版本。它检查对象是否具有所需的函数指针并发出调用。即使对象没有 releasebuffer 函数,此函数也会始终成功。
PyObject *PyObject_GetMemoryView(PyObject *obj)
从定义了缓冲区接口的对象返回一个内存视图对象。
内存视图对象是一个扩展的缓冲区对象,可以替换缓冲区对象(但不必这样做,因为可以将其保留为简单的 1 维内存视图对象)。其 C 结构如下所示:
typedef struct {
PyObject_HEAD
PyObject *base;
Py_buffer view;
} PyMemoryViewObject;
从功能上讲,这类似于当前的缓冲区对象,只是保留了对 base 的引用,并且不会重新获取内存视图。因此,此内存视图对象会一直持有 base 的内存,直到其被删除。
此内存视图对象将支持多维切片,并且是 Python 中提供的第一个支持此功能的对象。内存视图对象的切片是具有相同 base 但对 base 对象具有不同视图的其他内存视图对象。
当返回内存视图中的“元素”时,它始终是一个字节对象,其格式应由内存视图对象的 format 属性解释。如果需要,可以使用 struct 模块在 Python 中“解码”字节。或者,内容可以传递给 NumPy 数组或其他使用缓冲区协议的对象。
Python 名称将为
__builtin__.memoryview
方法
__getitem__
(将支持多维切片)__setitem__
(将支持多维切片)tobytes
(获取内存副本的新字节对象)。tolist
(获取内存的“嵌套”列表。所有内容都将解释为标准 Python 对象,就像 struct 模块的 unpack 所做的那样——实际上它使用 struct.unpack 来实现它)。属性(取自 base 对象的内存)
format
itemsize
shape
strides
suboffsets
readonly
ndim
Py_ssize_t PyBuffer_SizeFromFormat(const char *)
从结构体风格的描述返回数据格式区域隐含的 itemsize。
PyObject * PyMemoryView_GetContiguous(PyObject *obj, int buffertype,
char fortran)
返回一个指向由 obj 表示的连续内存块的 memoryview 对象。如果必须进行复制(因为 obj 指向的内存不连续),则将创建一个新的字节对象,并成为返回的 memoryview 对象的 base 对象。
buffertype 参数可以是 PyBUF_READ、PyBUF_WRITE、PyBUF_UPDATEIFCOPY,以确定返回的缓冲区是否应可读、可写或设置为在必须进行复制时更新原始缓冲区。如果 buffertype 为 PyBUF_WRITE 且缓冲区不连续,则将引发错误。在这种情况下,用户可以使用 PyBUF_UPDATEIFCOPY 确保返回可写的临时连续缓冲区。只要原始对象可写,在删除 memoryview 对象后,此连续缓冲区的内容将复制回原始对象。如果原始对象不允许这样做,则会引发 BufferError。
如果对象是多维的,那么如果 fortran 为 'F',则底层数组的第一维将在缓冲区中变化最快。如果 fortran 为 'C',则最后一维将变化最快(C 样式连续)。如果 fortran 为 'A',则无关紧要,您将获得对象认为最有效的方式。如果进行了复制,则必须通过调用 PyMem_Free
释放内存。
您将收到对内存视图对象的新的引用。
int PyObject_CopyToObject(PyObject *obj, void *buf, Py_ssize_t len,
char fortran)
将由 buf
指向的连续内存块指向的 len
字节数据复制到 obj 导出的缓冲区中。成功返回 0,失败返回 -1 并引发错误。如果对象没有可写缓冲区,则会引发错误。如果 fortran 为 'F',则如果对象是多维的,则数据将以 Fortran 样式(第一维变化最快)复制到数组中。如果 fortran 为 'C',则数据将以 C 样式(最后一维变化最快)复制到数组中。如果 fortran 为 'A',则无关紧要,复制将以任何更有效的方式进行。
int PyObject_CopyData(PyObject *dest, PyObject *src)
最后这三个 C-API 调用允许以标准方式将数据进出 Python 对象到连续内存区域,无论它是如何实际存储的。这些调用使用扩展缓冲区接口来执行其工作。
int PyBuffer_IsContiguous(Py_buffer *view, char fortran)
如果由视图对象定义的内存是 C 样式(fortran = 'C')或 Fortran 样式(fortran = 'F')连续或两者之一(fortran = 'A'),则返回 1。否则返回 0。
void PyBuffer_FillContiguousStrides(int ndim, Py_ssize_t *shape,
Py_ssize_t *strides, Py_ssize_t itemsize,
char fortran)
使用给定形状和每个元素的给定字节数的连续(如果 fortran 为 'C' 则为 C 样式,如果 fortran 为 'F' 则为 Fortran 样式)数组的字节步长填充 strides 数组。
int PyBuffer_FillInfo(Py_buffer *view, void *buf,
Py_ssize_t len, int readonly, int infoflags)
为只能共享给定长度的“无符号字节”的连续内存块的导出器正确填充缓冲区信息结构。成功返回 0,错误返回 -1(并引发错误)。
PyExc_BufferError
一个新的错误对象,用于返回由于导出器无法提供使用者期望的缓冲区类型而产生的缓冲区错误。当使用者从不提供协议的对象请求缓冲区时,也会引发此错误。
对struct字符串语法的补充
struct 字符串语法缺少一些字符来完全实现其他地方(例如 ctypes 和 NumPy 中)已经可用的数据格式描述。Python 2.5 规范位于 https://docs.pythonlang.cn/library/struct.html。
以下是建议的添加内容
字符 | 描述 |
---|---|
‘t’ | 位(前面的数字表示有多少位) |
‘?’ | 平台 _Bool 类型 |
‘g’ | 长双精度 |
‘c’ | ucs-1(latin-1)编码 |
‘u’ | ucs-2 |
‘w’ | ucs-4 |
‘O’ | 指向 Python 对象的指针 |
‘Z’ | 复数(下一个说明符是什么) |
‘&’ | 特定指针(另一个字符之前的前缀) |
‘T{}’ | 结构({} 内的详细布局) |
‘(k1,k2,…,kn)’ | 后面内容的多维数组 |
‘:name:’ | 前一个元素的可选名称 |
‘X{}’ |
|
struct 模块将被更改为也理解这些内容,并在解包时返回相应的 Python 对象。解包长双精度将返回十进制对象或 ctypes 长双精度。解包 'u' 或 'w' 将返回 Python unicode。解包多维数组将返回列表(如果 >1d 则为列表的列表)。解包指针将返回 ctypes 指针对象。解包函数指针将返回 ctypes 调用对象(也许)。解包位将返回 Python 布尔值。如果 struct 字符串语法中还没有空格,则会忽略空格。解包命名对象将返回某种类似命名元组的对象,该对象的行为类似于元组,但其条目也可以通过名称访问。解包嵌套结构将返回嵌套元组。
端序规范('!','@','=','>','<','^')也允许在字符串内,以便在需要时可以更改。先前指定的端序字符串一直有效,直到更改为止。默认端序为 '@',表示本机数据类型和对齐方式。如果请求未对齐的本机数据类型,则端序规范为 '^'。
根据 struct 模块,数字可以放在字符代码前面以指定该类型的数量。 (k1,k2,...,kn)
扩展还允许指定数据是否应被视为特定格式的(C 样式连续,最后一维变化最快)多维数组。
应向 ctypes 添加函数,以从 struct 描述创建 ctypes 对象,并将长双精度和 ucs-2 添加到 ctypes 中。
数据格式描述示例
以下是一些 C 结构的示例以及它们如何使用 struct 样式语法表示。
<named> 是命名元组的构造函数(尚未指定)。
- 浮点数
'd'
<–> Python 浮点数- 复数双精度
'Zd'
<–> Python 复数- RGB 像素数据
'BBB'
<–> (int, int, int)'B:r: B:g: B:b:'
<–> <named>((int, int, int), (‘r’,’g’,’b’))- 混合端序(奇怪但可能)
'>i:big: <i:little:'
<–> <named>((int, int), (‘big’, ‘little’))- 嵌套结构
struct { int ival; struct { unsigned short sval; unsigned char bval; unsigned char cval; } sub; } """i:ival: T{ H:sval: B:bval: B:cval: }:sub: """
- 嵌套数组
struct { int ival; double data[16*4]; } """i:ival: (16,4)d:data: """
请注意,在最后一个示例中,与之比较的 C 结构故意是一个一维数组,而不是一个二维数组 data[16][4]。这样做的原因是为了避免 C 中静态多维数组(以连续方式布局)和动态多维数组之间的混淆,动态多维数组使用相同的语法访问元素,data[0][1],但其内存不一定连续。struct 语法 *始终* 使用连续内存,多维字符是导出器要传达的有关内存的信息。
换句话说,struct 语法描述不必完全匹配 C 语法,只要它描述相同的内存布局即可。C 编译器将内存视为双精度数的一维数组这一事实与导出器希望向使用者传达此内存字段应被视为二维数组这一事实无关,其中新维度在每 4 个元素后考虑。
受影响的代码
导出或使用旧缓冲区接口的所有 Python 对象和模块都将被修改。这是一个部分列表。
- 缓冲区对象
- 字节对象
- 字符串对象
- unicode 对象
- 数组模块
- struct 模块
- mmap 模块
- ctypes 模块
任何其他使用缓冲区 API 的内容。
问题和细节
此 PEP 旨在通过向现有缓冲区协议添加 C-API 和两个函数来向后移植到 Python 2.6。
此 PEP 的先前版本提出了读/写锁定方案,但后来被认为 a) 对于不需要任何锁定的常见简单用例来说过于复杂,以及 b) 对于需要对缓冲区进行并发读/写访问并使用不断变化的、短暂的锁的用例来说过于简单。因此,如果用户需要跨并发读/写访问保持一致的视图,则由用户自行实施其特定的锁定方案。在获得一些使用这些用户方案的经验后,可能会提出一个未来的 PEP,其中包含一个单独的锁定 API。
步长内存和子偏移量的共享是新的,可以看作是对多段接口的修改。它是由 NumPy 和 PIL 推动的。NumPy 对象应该能够与其了解如何管理步长内存的代码共享其步长内存,因为步长内存在与计算库交互时非常常见。
此外,使用这种方法,应该可以编写适用于两种内存的通用代码,而无需复制。
缓冲区信息结构中格式字符串、形状数组、步长数组和子偏移量数组的内存管理始终是导出对象的责任。使用者不应将这些指针设置为任何其他内存或尝试释放它们。
讨论并拒绝了一些想法
有一个“释放器”对象,其 release-buffer 被调用。这被认为是不可接受的,因为它导致协议不对称(您在与“获取”缓冲区不同的内容上调用 release)。它还在没有提供真正好处的情况下使协议复杂化。将所有 struct 变量分别传递给函数。这样做的好处是可以将 NULL 设置为不感兴趣的变量,但它也使函数调用变得更加困难。flags 变量允许使用者在调用协议时“简单”地执行此操作。
代码
PEP 的作者承诺为该提案贡献和维护代码,但欢迎任何帮助。
示例
示例1
此示例显示了一个使用连续行的图像对象如何公开其缓冲区
struct rgba {
unsigned char r, g, b, a;
};
struct ImageObject {
PyObject_HEAD;
...
struct rgba** lines;
Py_ssize_t height;
Py_ssize_t width;
Py_ssize_t shape_array[2];
Py_ssize_t stride_array[2];
Py_ssize_t view_count;
};
“lines” 指向 (struct rgba*)
的已分配 1-D 数组。该块中的每个指针都指向一个单独分配的 (struct rgba)
数组。
为了访问,例如,x=30,y=50 处的像素的红色值,您将使用“lines[50][30].r”。
那么 ImageObject 的 getbuffer 做了什么?省略错误检查
int Image_getbuffer(PyObject *self, Py_buffer *view, int flags) {
static Py_ssize_t suboffsets[2] = { 0, -1};
view->buf = self->lines;
view->len = self->height*self->width;
view->readonly = 0;
view->ndims = 2;
self->shape_array[0] = height;
self->shape_array[1] = width;
view->shape = &self->shape_array;
self->stride_array[0] = sizeof(struct rgba*);
self->stride_array[1] = sizeof(struct rgba);
view->strides = &self->stride_array;
view->suboffsets = suboffsets;
self->view_count ++;
return 0;
}
int Image_releasebuffer(PyObject *self, Py_buffer *view) {
self->view_count--;
return 0;
}
示例2
此示例显示了一个想要公开连续内存块(在对象处于活动状态时永远不会重新分配)的对象将如何执行此操作。
int myobject_getbuffer(PyObject *self, Py_buffer *view, int flags) {
void *buf;
Py_ssize_t len;
int readonly=0;
buf = /* Point to buffer */
len = /* Set to size of buffer */
readonly = /* Set to 1 if readonly */
return PyObject_FillBufferInfo(view, buf, len, readonly, flags);
}
/* No releasebuffer is necessary because the memory will never
be re-allocated
*/
示例3
想要从 Python 对象 obj 获取简单连续字节块的使用者将执行以下操作
Py_buffer view;
int ret;
if (PyObject_GetBuffer(obj, &view, Py_BUF_SIMPLE) < 0) {
/* error return */
}
/* Now, view.buf is the pointer to memory
view.len is the length
view.readonly is whether or not the memory is read-only.
*/
/* After using the information and you don't need it anymore */
if (PyBuffer_Release(obj, &view) < 0) {
/* error return */
}
示例4
想要能够使用任何对象的内存但正在编写仅处理连续内存的算法的使用者可以执行以下操作
void *buf;
Py_ssize_t len;
char *format;
int copy;
copy = PyObject_GetContiguous(obj, &buf, &len, &format, 0, 'A');
if (copy < 0) {
/* error return */
}
/* process memory pointed to by buffer if format is correct */
/* Optional:
if, after processing, we want to copy data from buffer back
into the object
we could do
*/
if (PyObject_CopyToObject(obj, buf, len, 'A') < 0) {
/* error return */
}
/* Make sure that if a copy was made, the memory is freed */
if (copy == 1) PyMem_Free(buf);
版权
此 PEP 放入公有领域。
来源: https://github.com/python/peps/blob/main/peps/pep-3118.rst
上次修改: 2023-09-09 17:39:29 GMT