CPython MemoryView C API 详解:零拷贝缓冲区访问与 PyMemoryView_* 函数全集
本文基于 CPython 官方文档 Doc/c-api/memoryview.rst 展开,系统讲解 C 扩展中操作 memoryview 对象的全部 C-API:类型对象 PyMemoryView_Type、四个构造函数(PyMemoryView_FromObject、PyMemoryView_FromMemory、PyMemoryView_FromBuffer、PyMemoryView_GetContiguous)、类型检查 PyMemoryView_Check 以及底层访问宏 PyMemoryView_GET_BUFFER / PyMemoryView_GET_BASE。读完本文,你将能在 C 扩展中安全地创建、检查和读取 memoryview,并结合 Objects/memoryobject.c 的源码理解“受管缓冲区快照”机制——CPython 用来保证链式 memoryview 行为一致的核心设计。
memoryview 在缓冲区协议中的位置
memoryview 对象将 C 层面的 buffer protocol 暴露为普通的 Python 对象,使其可以像其他对象一样被传递、存储和索引。这一角色在缓冲区协议文档中被明确定义:buffer 结构(Py_buffer)本身是简单的 C 结构体而非 PyObject 指针,可以廉价地创建和复制;当需要一个通用的 buffer 包装器时,就可以创建 memoryview 对象。
缓冲区协议有两个方向:
- 导出方(producer):类型(如
bytes、bytearray、array.array,以及第三方扩展类型)通过getbufferproc暴露底层 buffer 信息; - 消费方(consumer):通过
PyObject_GetBuffer()或PyArg_ParseTuple的y*/w*/s*格式码获取原始数据指针,用完必须调用PyBuffer_Release()。
memoryview 是消费方在 Python 层的具象化:PyMemoryView_FromObject() 内部请求 PyBUF_FULL_RO 级别的完整信息(见 Objects/memoryobject.c#L853-L857),因此 memoryview 总是拥有完整的 shape/strides/format 描述,这也是它能支持多维切片和 tolist() 等操作的底层原因。
PyMemoryView_Type 与 PyMemoryView_Check
PyMemoryView_Type 是代表 Python 层 memoryview 类型的 PyTypeObject 实例——它就是你在解释器中 type(memoryview(b'')) 得到的那个类型对象,C 扩展可以拿它做类型注册、isinstance 判断等。
文档中同时给出检查函数:
int PyMemoryView_Check(PyObject *obj);
返回真值当且仅当 obj 是 memoryview 对象。文档特别指出:目前不允许创建 memoryview 的子类,因此该检查总是成功的(不会抛错)。这一说法与当前仓库的宏定义完全吻合,Include/memoryobject.h#L11 中它就是严格相等判断:
#define PyMemoryView_Check(op) Py_IS_TYPE((op), &PyMemoryView_Type)
即 Py_TYPE(op) == &PyMemoryView_Type。正因为没有子类,CPython 可以直接用“类型指针相等”代替更昂贵也更宽松的 PyType_IsSubtype 检查——这是一种性能与语义的折中:任何伪造或子类化的对象都会被拒绝。
在 C 扩展中典型的用法是:
static PyObject *
demo_show(PyObject *module, PyObject *obj)
{
if (!PyMemoryView_Check(obj)) {
PyErr_SetString(PyExc_TypeError, "expected a memoryview");
return NULL;
}
Py_buffer *view = PyMemoryView_GET_BUFFER(obj);
printf("buf=%p len=%zd ndim=%d readonly=%d\n",
view->buf, view->len, view->ndim, view->readonly);
Py_RETURN_NONE;
}
构造函数之一:PyMemoryView_FromObject
PyObject *PyMemoryView_FromObject(PyObject *obj);
从一个实现了缓冲区协议的对象创建 memoryview。如果 obj 支持可写 buffer 导出,得到的 memoryview 就是可读写(read/write)的;否则它可能是只读的,也可能是可写的——由导出方自行决定。
从 Objects/memoryobject.c#L853-L857 的实现看,该函数直接委托给内部函数 PyMemoryView_FromObjectAndFlags(v, PyBUF_FULL_RO),后者有两条路径:
v本身是 memoryview:不创建新的受管缓冲区,而是调用mbuf_add_view()注册到同一个 managed buffer 上(Objects/memoryobject.c#L800-L825)。这保证了对 memoryview 再套 memoryview 时,所有链式视图共享同一份 buffer 快照,避免了 PEP-3118 允许的“底层对象在视图导出期间发生变化”带来的不一致问题;v是其他支持缓冲区协议的对象:通过_PyManagedBuffer_FromObject()调用PyObject_GetBuffer()生成一份 master 快照,再挂上 memoryview。
如果对象既不是 memoryview 也不支持缓冲区协议,抛出 TypeError: memoryview: a bytes-like object is required, not 'xxx'——这正是你在 Python 层执行 memoryview(42) 时看到的错误,Python 层构造函数最终就是走到这条 C 路径。
构造函数之二:PyMemoryView_FromMemory 与 PyBUF_READ/PyBUF_WRITE
PyObject *PyMemoryView_FromMemory(char *mem, Py_ssize_t size, int flags);
(Python 3.3 引入。)用裸内存块 mem 作为底层 buffer 创建 memoryview。flags 只能取两个值:
PyBUF_READ(0x100):请求只读 buffer;PyBUF_WRITE(0x200):请求可写 buffer。
这两个宏定义在 Include/pybuffer.h#L137-L138。它们与 PyArg_ParseTuple 的 y*(只读)/w*(可写)格式码语义对应。
实现上(Objects/memoryobject.c#L740-L762),该函数:
readonly = (flags == PyBUF_WRITE) ? 0 : 1;
(void)PyBuffer_FillInfo(&mbuf->master, NULL, mem, size, readonly,
PyBUF_FULL_RO);
mv = mbuf_add_view(mbuf, NULL);
即通过 PyBuffer_FillInfo() 以“连续的 unsigned bytes(格式串 "B")”语义填充 master buffer,obj 字段为 NULL——因为这块内存并不属于任何 Python 对象。注意其内部断言 flags == PyBUF_READ || flags == PyBUF_WRITE,传入其他标志是未定义行为。
由此可以得出两条实践要点:
- 用
PyBUF_WRITE创建的可写 memoryview 会直接写穿到你提供的mem区域,生命周期完全由 C 侧负责,必须保证内存块在 memoryview 存活期间有效; - 由于没有导出对象,后续
PyMemoryView_GET_BASE()会返回NULL,这正是文档对GET_BASE语义的说明来源之一。
构造函数之三:PyMemoryView_FromBuffer
PyObject *PyMemoryView_FromBuffer(const Py_buffer *view);
包装一个已经填好的 Py_buffer 结构来创建 memoryview。文档明确:对简单的字节 buffer,优先使用 PyMemoryView_FromMemory();FromBuffer 是处理多维、带 strides/格式信息等复杂情况的入口。
实现细节值得注意(Objects/memoryobject.c#L769-L794):
if (info->buf == NULL) {
PyErr_SetString(PyExc_ValueError,
"PyMemoryView_FromBuffer(): info->buf must not be NULL");
return NULL;
}
mbuf->master = *info;
mbuf->master.obj = NULL; /* info->obj is a borrowed reference, do NOT decrement */
info->buf为NULL时抛ValueError;- 整个结构体被按值拷入 master buffer,且
obj被强制置NULL——源码注释指出传入的info->obj是借用引用,PyBuffer_Release()不应该对其减引用。这是调用方最容易踩的引用计数坑; - 由于这是唯一可以创建“信息不完整”的 master buffer 的入口,内部
init_shape_strides()需要能够重建缺失的 shape/strides(例如只给了ndim、itemsize、len的简易 buffer)。
Py_buffer 结构本身的定义见 Include/pybuffer.h#L20-L33,其布局与大小自 Python 3.11 起属于稳定 ABI 的一部分,不得随意变更——使用 Limited API 时这一点尤其关键。
构造函数之四:PyMemoryView_GetContiguous
PyObject *PyMemoryView_GetContiguous(PyObject *obj, int buffertype, char order);
从任意支持缓冲区协议的对象创建指向连续内存块的 memoryview,order 指定连续性方向:'C'(C 顺序)或 'F'(Fortran 顺序)。文档给出的核心行为:
- 如果内存本身已经按目标方向连续,memoryview 直接指向原内存(零拷贝);
- 否则做一次拷贝,返回的 memoryview 指向一个新建的
bytes对象。
buffertype 取 PyBUF_READ 或 PyBUF_WRITE。Objects/memoryobject.c#L965-L1000 的实现精确对应了文档描述,并补充了两种错误情形:
if (buffertype == PyBUF_WRITE && view->readonly)
PyErr_SetString(PyExc_BufferError, "underlying buffer is not writable");
if (PyBuffer_IsContiguous(view, order))
return (PyObject *)mv; /* 已连续:原样返回,零拷贝 */
if (buffertype == PyBUF_WRITE)
PyErr_SetString(PyExc_BufferError,
"writable contiguous buffer requested for a non-contiguous object.");
ret = memory_from_contiguous_copy(view, order); /* 只读且非连续:拷贝到 bytes */
也就是说:请求可写连续 buffer 但源既不可写又非连续时,会抛 BufferError 而不是静默拷贝——因为拷贝出来的是 bytes 副本,写它并不能写回源对象,语义上必须显式失败。order 传 'A'(Any)时,'C'/'A' 会按 C 序重排,'F' 按 Fortran 序重排(见 memory_from_contiguous_copy() 中 init_strides_from_shape() / init_fortran_strides_from_shape() 的选择)。
这个函数的典型用途是:扩展代码需要一个“按某方向连续”的内存块传给只接受连续内存的第三方 C 库,同时不想自己实现拷贝逻辑。
底层访问宏:PyMemoryView_GET_BUFFER 与 PyMemoryView_GET_BASE
拿到 memoryview 后,两个宏提供直接访问:
Py_buffer *PyMemoryView_GET_BUFFER(PyObject *mview);
返回指向该 memoryview 私有的、导出方 buffer 副本的指针。文档用加粗强调了约束:mview 必须是 memoryview 实例;该宏不检查类型,检查是你自己的责任,否则可能崩溃。
PyObject *PyMemoryView_GET_BASE(PyObject *mview);
返回 memoryview 所基于的导出对象;若该 memoryview 由 PyMemoryView_FromMemory() 或 PyMemoryView_FromBuffer() 创建,则返回 NULL。mview 同样必须是 memoryview 实例。
在当前仓库中,这两个“宏”实际上是 Include/cpython/memoryobject.h#L41-L50 中的 static inline 函数(外加兼容宏):
static inline Py_buffer* PyMemoryView_GET_BUFFER(PyObject *op) {
return (&_PyMemoryView_CAST(op)->view);
}
static inline PyObject* PyMemoryView_GET_BASE(PyObject *op) {
return _PyMemoryView_CAST(op)->view.obj;
}
它们直接读取 PyMemoryViewObject 的 view 字段。从 Include/cpython/memoryobject.h#L27-L36 的结构体定义可以看清 memoryview 的完整内部布局:
typedef struct {
PyObject_VAR_HEAD
_PyManagedBufferObject *mbuf; /* managed buffer */
Py_hash_t hash; /* hash value for read-only views */
int flags; /* state flags */
Py_ssize_t exports; /* number of buffer re-exports */
Py_buffer view; /* private copy of the exporter's view */
PyObject *weakreflist;
Py_ssize_t ob_array[1]; /* shape, strides, suboffsets */
} PyMemoryViewObject;
头文件开头有一段重要警告:这些结构体之所以在此声明,只是为了让宏能工作,不应被视为公共接口,不要直接访问字段,应使用宏和函数。hash 字段也顺带解释了 Python 层“只读 memoryview 可哈希、可作字典键”的特性来源。
源码纵深:受管缓冲区(managed buffer)机制
文档只描述了各函数的表面行为,而 Objects/memoryobject.c 开头的设计注释解释了更深层的机制:managed buffer(_PyManagedBufferObject,定义于 Include/cpython/memoryobject.h#L11-L16)。
PEP-3118 允许底层导出对象在视图导出期间发生变化(例如 bytearray 被原地修改后重新导出 buffer)。如果链式 memoryview 每次都把 buffer 请求重定向到原始 base 对象,可能得到意外结果。CPython 的解法是:
- 构造函数
_PyManagedBuffer_FromObject()在第一次导出时生成唯一的 buffer 快照(master buffer); - 之后所有链式 memoryview(包括对 memoryview 再取 memoryview)都注册到这同一个 managed buffer 上,共享这份快照;
- master 中的 shape/strides/suboffsets/format 对所有消费者只读,而每个 memoryview 持有自己私有的
view副本,其中的 shape/strides/suboffsets 归该 memoryview 所有、可写(用于切片时重建布局)。
引用计数规则同样在注释中写明:Py_buffer.obj 若非 NULL 必须指向导出 base 对象且持有新引用,PyBuffer_Release() 负责对其减引用并置 NULL,因此各类型的 releasebufferproc 不得重复对 view.obj 减引用——这是写 C 扩展 buffer 导出方时最经典的 double-free 来源。
managed buffer 还维护 exports 计数与释放标记(_Py_MANAGED_BUFFER_RELEASED)。当底层关系被清理时,对已释放的 memoryview 再操作会触发 ValueError: operation forbidden on released memoryview(见 Objects/memoryobject.c#L180-L196 的 CHECK_RELEASED 宏)——如果你在处理大对象析构周期中遇到这个报错,根源就在这里。
相关 flag 速查与延伸阅读
memoryview 文档只用到 PyBUF_READ/PyBUF_WRITE,但 Include/pybuffer.h#L104-L134 定义了完整的 flag 体系,写缓冲区协议代码时值得对照(维度上限 PyBUF_MAX_NDIM 为 64,见 Include/pybuffer.h#L105):
| 宏 | 值 | 含义 |
|---|---|---|
PyBUF_SIMPLE |
0 |
仅要连续字节串 |
PyBUF_WRITABLE |
0x0001 |
要求可写 |
PyBUF_FORMAT |
0x0004 |
要格式串 |
PyBUF_ND |
0x0008 |
要 shape |
PyBUF_STRIDES |
0x0010 | PyBUF_ND |
要 strides(隐含 ndim) |
PyBUF_C_CONTIGUOUS / PyBUF_F_CONTIGUOUS |
0x0020 / 0x0040(均含 STRIDES) |
要求 C/F 连续 |
PyBUF_ANY_CONTIGUOUS |
0x0080 | PyBUF_STRIDES |
任一方向连续即可 |
PyBUF_INDIRECT |
0x0100 | PyBUF_STRIDES |
允许 suboffsets(PIL 风格) |
PyBUF_CONTIG / PyBUF_CONTIG_RO |
PyBUF_ND | PyBUF_WRITABLE / PyBUF_ND |
连续,可写/只读 |
PyBUF_RECORDS / PyBUF_RECORDS_RO |
STRIDES[ | WRITABLE] | FORMAT |
完整记录语义 |
PyBUF_FULL / PyBUF_FULL_RO |
INDIRECT | [WRITABLE] | FORMAT |
全部信息 |
PyBUF_READ / PyBUF_WRITE |
0x100 / 0x200 |
memoryview 构造用的读/写标记 |
组合规则是“请求越详细,flag 越多位”:例如 PyBUF_RECORDS 已经隐含 strides 与 ndim,调用 PyObject_GetBuffer() 时按此组合即可。
延伸阅读与验证材料:
- Doc/c-api/buffer.rst:缓冲区协议的完整 C-API 文档(
Py_buffer各字段语义、PyObject_GetBuffer、PyBuffer_FillInfo等),是 memoryview 文档的姊妹篇,文中标记为bufferobjects的引用锚点即出自此文件; - Include/memoryobject.h:
PyMemoryView_Check与PyMemoryView_FromObject的公共声明; - Include/pybuffer.h:
Py_buffer结构、PyBuffer_*工具函数族与全部 flag 定义; - Objects/memoryobject.c:managed buffer 与 memoryview 的完整实现,包括多维拷贝(
copy_buffer())、C/F 连续 strides 初始化等; - Lib/test/test_buffer.py:缓冲区协议与 memoryview 行为的测试用例集,覆盖
PyMemoryView_FromMemory、PyMemoryView_FromBuffer、PyMemoryView_GetContiguous等的边界行为; - Lib/test/test_stable_abi_ctypes.py:通过 ctypes 验证上述函数在稳定 ABI 下的可用性。
小结
memoryview 的 C-API 表面上只有七个符号,职责却划分得很清晰:PyMemoryView_Type 提供类型身份,PyMemoryView_Check 做严格类型判定;FromObject/FromMemory/FromBuffer 分别对应“包装 Python 对象”“包装裸内存”“包装现成 Py_buffer”三种来源;GetContiguous 解决“必须要连续内存”的常见诉求并以零拷贝为默认路径;GET_BUFFER/GET_BASE 则是在确认类型后提取底层数据的最后一步。理解了 managed buffer 的快照语义与 obj 字段的引用计数规则,你就能在扩展模块中安全地跨语言共享大块内存,而不必在 Python 层反复复制数据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00