PEP 3118 – 修订缓冲区协议
- 作者:
- Travis Oliphant <oliphant at ee.byu.edu>, Carl Banks <pythondev at aerojockey.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2006年8月28日
- Python 版本:
- 3.0
- 发布历史:
- 2007年4月9日
摘要
本 PEP 提议重新设计缓冲区接口(PyBufferProcs 函数指针),以改进 Python 3.0 中 Python 允许内存共享的方式。
特别是,建议取消 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_CONTIGUOUSPyBUF_F_CONTIGUOUSPyBUF_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 在标志参数中时,才保证格式信息不为 NULL,否则,消费者应假定为无符号字节。
如果只能导出连续的“无符号字节”块,则简单的导出对象可以使用 C-API 来根据提供的标志正确填充缓冲区信息结构。
Py_buffer 结构
缓冲区信息结构为
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 函数之前,缓冲区信息结构可以填充任何内容,但在请求新缓冲区时,buf 字段必须为 NULL。从 bf_getbuffer 返回后,缓冲区信息结构将填充有关缓冲区相关信息。当消费者使用完内存后,必须将相同的缓冲区信息结构传递给 bf_releasebuffer(如果可用)。调用者负责保留对 obj 的引用,直到调用 releasebuffer(即调用 bf_getbuffer 不会更改 obj 的引用计数)。
缓冲区信息结构的成员包括:
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。为了清楚起见,这里有一个函数,当同时存在非 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; }
请注意,子偏移量是在解引用“之后”添加的。因此,在第 i 维中切片会增加第 (i-1) 维中的子偏移量。在第一维中切片会直接更改起始指针的位置(即 buf 将被修改)。
itemsize- 这是共享内存中每个元素的项大小(以字节为单位)的存储。从技术上讲,它不是必需的,因为可以使用
PyBuffer_SizeFromFormat获取,但是导出器可能无需解析格式字符串即可知道此信息,并且为了正确解释步幅,了解项大小是必需的。因此,存储它更方便快捷。 internal- 这供导出对象内部使用。例如,导出器可能会将其重新转换为整数,并用于存储有关在释放缓冲区时是否必须释放形状、步幅和子偏移量数组的标志。消费者永远不应更改此值。
导出器负责确保 buf、format、shape、strides 和 suboffsets 指向的任何内存在其 releasebuffer 调用之前保持有效。如果导出器希望在 releasebuffer 调用之前能够更改对象的形状、步幅和/或子偏移量,则它应在调用 getbuffer 时分配这些数组(指向提供的缓冲区信息结构中)并在调用 releasebuffer 时释放它们。
释放缓冲区
在释放缓冲区接口调用中应使用相同的缓冲区信息结构。调用者负责 Py_buffer 结构本身的内存。
typedef void (*releasebufferproc)(PyObject *obj, Py_buffer *view)
getbufferproc 的调用者必须确保当不再需要从对象获取的内存时调用此函数。接口的导出者必须确保缓冲区信息结构中指向的任何内存保持有效,直到调用 releasebuffer。
如果未提供 bf_releasebuffer 函数(即为 NULL),则无需调用它。
如果导出器可以重新分配其内存、步幅、形状、子偏移量或格式变量(可能通过结构体 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)
从定义缓冲区接口的对象返回一个 memory-view 对象。
memory-view 对象是一个扩展的缓冲区对象,可以替换缓冲区对象(但不必如此,因为它可以保留为简单的 1 维 memory-view 对象)。它的 C 结构是:
typedef struct {
PyObject_HEAD
PyObject *base;
Py_buffer view;
} PyMemoryViewObject;
这在功能上与当前的缓冲区对象相似,只是保留了对 base 的引用,并且内存视图没有重新获取。因此,此内存视图对象会一直持有 base 的内存,直到它被删除。
这个 memory-view 对象将支持多维切片,并且是 Python 中第一个支持此功能的对象。memory-view 对象的切片是其他 memory-view 对象,它们具有相同的 base,但对 base 对象有不同的视图。
当从内存视图返回一个“元素”时,它始终是一个字节对象,其格式应由内存视图对象的 format 属性解释。如果需要,可以使用 struct 模块在 Python 中“解码”字节。或者可以将内容传递给 NumPy 数组或消耗缓冲区协议的其他对象。
Python 名称将是
__builtin__.memoryview
方法
__getitem__ (将支持多维切片)__setitem__ (将支持多维切片)tobytes (获取内存副本的新字节对象)。tolist (获取内存的“嵌套”列表。所有内容都将解释为标准 Python 对象,就像 struct 模块的 unpack 所做的那样——实际上它使用了 struct.unpack 来实现)。属性(取自基本对象的内存)
formatitemsizeshapestridessuboffsetsreadonlyndim
Py_ssize_t PyBuffer_SizeFromFormat(const char *)
从结构体样式描述中返回数据格式区域的隐含项大小。
PyObject * PyMemoryView_GetContiguous(PyObject *obj, int buffertype,
char fortran)
将内存视图对象返回到由 obj 表示的连续内存块。如果必须进行复制(因为 obj 指向的内存不连续),则将创建一个新的字节对象,并成为返回的内存视图对象的基本对象。
buffertype 参数可以是 PyBUF_READ、PyBUF_WRITE、PyBUF_UPDATEIFCOPY,用于确定返回的缓冲区应该是可读、可写,还是在必须进行复制时设置为更新原始缓冲区。如果 buffertype 为 PyBUF_WRITE 且缓冲区不连续,则会引发错误。在这种情况下,用户可以使用 PyBUF_UPDATEIFCOPY 来确保返回一个可写的临时连续缓冲区。只要原始对象是可写的,此连续缓冲区的内容将在内存视图对象删除后复制回原始对象。如果原始对象不允许这样做,则会引发 BufferError。
如果对象是多维的,那么如果 fortran 是“F”,则底层数组的第一维在缓冲区中变化最快。如果 fortran 是“C”,则最后一维变化最快(C 风格连续)。如果 fortran 是“A”,则无关紧要,您将获得对象认为更高效的任何方式。如果进行了复制,则必须通过调用 PyMem_Free 来释放内存。
您将收到 memoryview 对象的新引用。
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 风格)的字节步幅填充步幅数组,并指定每个元素的字节数。
int PyBuffer_FillInfo(Py_buffer *view, void *buf,
Py_ssize_t len, int readonly, int infoflags)
为只能共享给定长度的“无符号字节”连续内存块的导出器正确填充缓冲区信息结构。成功时返回 0,错误时返回 -1(并引发错误)。
PyExc_BufferError
一个新的错误对象,用于返回因导出者无法提供消费者期望的缓冲区类型而导致的缓冲区错误。当消费者从不提供协议的对象请求缓冲区时,也会引发此错误。
结构体字符串语法扩展
结构体字符串语法缺少一些字符,无法完全实现其他地方已有的数据格式描述(例如在 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 对象。解包 long-double 将返回一个 decimal 对象或 ctypes long-double。解包 'u' 或 'w' 将返回 Python unicode。解包多维数组将返回一个列表(如果 >1d 则为列表的列表)。解包指针将返回一个 ctypes 指针对象。解包函数指针将返回一个 ctypes call-object(可能)。解包一个位将返回一个 Python Bool。struct-string 语法中的空格将被忽略(如果尚未忽略)。解包命名对象将返回某种类似命名元组的对象,其行为像元组,但其条目也可以通过名称访问。解包嵌套结构将返回一个嵌套元组。
字符串中也允许使用字节序规范('!'、'@'、'='、'>'、'<'、'^'),以便在需要时进行更改。先前指定的字节序字符串将一直有效,直到更改。默认字节序是 '@',这意味着原生数据类型和对齐。如果请求不对齐的原生数据类型,则字节序规范为 '^'。
根据 struct 模块,字符代码前面可以有一个数字,以指定该类型的数量。(k1,k2,...,kn) 扩展还允许指定数据是否应被视为特定格式的(C 风格连续,最后一维变化最快)多维数组。
应该向 ctypes 添加函数,以从结构描述创建 ctypes 对象,并向 ctypes 添加 long-double 和 ucs-2。
数据格式描述示例
以下是一些 C 结构体及其如何使用结构体风格语法表示的示例。
- 浮点数
'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 结构体特意是 1 维数组,而不是 2 维数组 data[16][4]。这样做的原因是为了避免 C 语言中静态多维数组(它们是连续布局的)和动态多维数组之间的混淆,后者使用相同的语法访问元素 data[0][1],但其内存不一定是连续的。结构体语法**总是**使用连续内存,多维特性是导出者要传达给消费者的内存信息。
换句话说,结构体语法描述不必与 C 语法完全匹配,只要它描述了相同的内存布局即可。C 编译器会将内存视为一维双精度浮点数数组这一事实,与导出者希望告知消费者内存的这个字段应被视为二维数组(每 4 个元素后被视为新维度)这一事实无关。
受影响的代码
所有导出或使用旧缓冲区接口的 Python 对象和模块都将进行修改。以下是部分列表:
- 缓冲区对象
- 字节对象
- 字符串对象
- Unicode 对象
- array 模块
- struct 模块
- mmap 模块
- ctypes 模块
任何其他使用缓冲区 API 的东西。
问题与细节
本 PEP 旨在通过向现有缓冲区协议添加 C-API 和两个函数来向后移植到 Python 2.6。
本 PEP 的先前版本提出了读/写锁定方案,但后来被认为 a) 对于不需要任何锁定的常见简单用例来说过于复杂,b) 对于需要对缓冲区进行并发读/写访问且锁定频繁且短暂的用例来说过于简单。因此,用户如果需要跨并发读/写访问的一致视图,则需自行围绕缓冲区对象实现自己的特定锁定方案。未来可能会提出一个 PEP,其中包括在获得这些用户方案的一些经验后引入单独的锁定 API。
步幅内存和子偏移量的共享是新的,可以看作是多段接口的修改。它是由 NumPy 和 PIL 推动的。NumPy 对象应该能够与理解如何管理步幅内存的代码共享其步幅内存,因为在与计算库交互时,步幅内存非常常见。
此外,通过这种方法,应该可以编写通用代码,无需复制即可同时处理两种内存。
缓冲区信息结构中格式字符串、形状数组、步幅数组和子偏移量数组的内存管理始终是导出对象的责任。消费者不应将这些指针设置为任何其他内存或尝试释放它们。
讨论并拒绝了几种想法:
有一个“释放器”对象,其 release-buffer 被调用。这被认为不可接受,因为它导致协议不对称(你对不同于你“获取”缓冲区的对象调用释放)。它还使协议复杂化,而没有提供真正的益处。将所有结构体变量单独传递给函数。这具有的优点是允许将不感兴趣的变量设置为 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”指向 malloc 分配的 (struct rgba*) 的一维数组。该块中的每个指针都指向单独 malloc 分配的 (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