CPython bytearray C API 详解:PyByteArrayObject、PyByteArray_Type 与 PyByteArray_* 系列接口全解
在 C 扩展中处理二进制数据时,bytearray 是唯一一个「可变」的字节序列类型,也是 buffer 协议的典型消费场景。本文基于 CPython 官方文档 Byte Array Objects 与源码实现 bytearrayobject.c,完整讲解 PyByteArrayObject 类型、PyByteArray_Type 类型对象、类型检查宏、六个 Direct API 函数以及两个「以安全换速度」的宏:从每个函数的签名、返回值约定、异常语义,到内部的内存布局、resize 策略与线程安全机制,帮助你在编写扩展模块时正确、高效、线程安全地操作 bytearray 对象。
一、核心类型:PyByteArrayObject 与 PyByteArray_Type
文档 Doc/c-api/bytearray.rst 首先定义了两个基础符号:
PyByteArrayObject:表示 Python bytearray 对象的PyObject子类型;PyByteArray_Type:PyTypeObject类型的实例,即 Python 层bytearray类本身的类型对象,是扩展模块构造、检查 bytearray 的锚点。
二者的声明位于 bytearrayobject.h:
/* Type object */
PyAPI_DATA(PyTypeObject) PyByteArray_Type;
PyAPI_DATA(PyTypeObject) PyByteArrayIter_Type;
/* Direct API functions */
PyAPI_FUNC(PyObject *) PyByteArray_FromObject(PyObject *);
PyAPI_FUNC(PyObject *) PyByteArray_Concat(PyObject *, PyObject *);
PyAPI_FUNC(PyObject *) PyByteArray_FromStringAndSize(const char *, Py_ssize_t);
PyAPI_FUNC(Py_ssize_t) PyByteArray_Size(PyObject *);
PyAPI_FUNC(char *) PyByteArray_AsString(PyObject *);
PyAPI_FUNC(int) PyByteArray_Resize(PyObject *, Py_ssize_t);
头文件开头的注释(第 9-17 行)点明了 bytearray 的设计定位:它是「可变的字节数组」,Python 层 API 表现为序列,每个字节映射为 [0, 256) 的整数;字节不是字符,与 str 之间的转换必须经过编码/解码;为了 C 程序员方便,bytes 类型被视为 char* 而非 unsigned char*。
从源码结构看,PyByteArrayObject 的内存布局定义在 cpython/bytearrayobject.h:
typedef struct {
PyObject_VAR_HEAD
Py_ssize_t ob_alloc; /* ob_bytes 中已分配的字节数,原子读写 */
char *ob_bytes; /* 物理后备缓冲区 */
char *ob_start; /* ob_bytes 内的逻辑起点 */
Py_ssize_t ob_exports; /* 当前 buffer 导出数量 */
PyObject *ob_bytes_object; /* PyBytes,用于零拷贝的 bytes 转换 */
} PyByteArrayObject;
几个字段值得注意:
ob_bytes/ob_start分离:物理缓冲区与逻辑起点可以不一致。缩小操作(如del ba[0:n]或take_bytes)会先把ob_start前移而不是真正释放内存,这在 bytearrayobject.c 的bytearray_setslice_linear中可以看到,只有当缩减幅度很大时才会真正重分配;ob_exports是 buffer 导出的计数。每当通过 buffer 协议导出一块视图(如 memoryview),计数加一(bytearray_getbuffer 中obj->ob_exports++);只要存在导出,对象就不能被 resize,否则导出视图会指向已失效的内存。检查逻辑在_canresize(bytearrayobject.c):
static int
_canresize(PyByteArrayObject *self)
{
if (self->ob_exports > 0) {
PyErr_SetString(PyExc_BufferError,
"Existing exports of data: object cannot be re-sized");
return 0;
}
return 1;
}
ob_bytes_object指向一个bytes对象,bytearray 的实际数据直接存放在该 bytes 的后端内存中(见bytearray_reinit_from_bytes,bytearrayobject.c),这使得「bytearray 与 bytes 互换」可以做到零拷贝。
类型对象 PyByteArray_Type 的完整定义在 bytearrayobject.c。从源码结构看,它带有 Py_TPFLAGS_BASETYPE 标志,因此 bytearray 允许被 C 层面继承;tp_richcompare 通过 buffer 协议实现「bytearray 可与任何支持 buffer API 的对象比较」;tp_dealloc 则会在释放时检查 ob_exports,若仍有导出会打印 SystemError(bytearrayobject.c)。
二、类型检查宏:PyByteArray_Check 与 PyByteArray_CheckExact
文档给出两个总是成功(always succeeds)的检查函数:
int PyByteArray_Check(PyObject *o):当o是 bytearray 或其子类型的实例 时返回真;int PyByteArray_CheckExact(PyObject *o):当o恰好是 bytearray、不是子类型实例时返回真。
在 bytearrayobject.h 中它们被实现为宏:
#define PyByteArray_Check(self) PyObject_TypeCheck((self), &PyByteArray_Type)
#define PyByteArray_CheckExact(self) Py_IS_TYPE((self), &PyByteArray_Type)
注意 PyByteArray_Check 基于 PyObject_TypeCheck,会走 MRO 查找,因此对 bytearray 子类返回真;PyByteArray_CheckExact 只比较 Py_TYPE 指针,不做继承判断,速度更快。测试用例 test_bytearray.py 精确验证了这一点:对 bytearray(b'abc') 两者都为真,对子类 ByteArraySubclass(b'abc') 只有 Check 为真,对 bytes、int、list、实现了 __bytes__ 的普通对象均为假。选择建议:需要「字节序列数据」语义(可能接受子类)时用 PyByteArray_Check;只信任精确类型、避免子类覆写行为时用 PyByteArray_CheckExact。
三、创建 bytearray:PyByteArray_FromObject 与 PyByteArray_FromStringAndSize
3.1 PyByteArray_FromObject:从任意 buffer 协议对象构造
PyObject* PyByteArray_FromObject(PyObject *o)
对任何实现了 buffer 协议 的对象 o 返回一个新的 bytearray。失败时返回 NULL 并设置异常。文档特别提示:若对象实现了 buffer 协议,在创建 bytearray 期间该 buffer 不得被修改。
源码实现只有一行(bytearrayobject.c):
PyObject *
PyByteArray_FromObject(PyObject *input)
{
return PyObject_CallOneArg((PyObject *)&PyByteArray_Type, input);
}
即等价于 Python 层的 bytearray(o),因此它接受 bytes、bytearray 子类、list 等一切 bytearray() 构造器能接受的输入。test_fromobject 验证:fromobject(b'abc')、fromobject(bytearray(b'abc')) 得到 bytearray(b'abc'),fromobject([97, 98, 99]) 得到 bytearray(b'abc'),fromobject(3) 得到 bytearray(b'\0\0\0')(整数参数表示「创建 N 个零字节」),而传入字符串 object() 或不可 buffer 化的对象则抛 TypeError。
内部还有一个私有路径 _PyByteArray_FromBufferObject(bytearrayobject.c),它先用 PyObject_GetBuffer(obj, &view, PyBUF_FULL_RO) 取只读全视图,再 PyByteArray_FromStringAndSize 建好目标,最后 PyBuffer_ToContiguous 把源数据(可能是非连续的,比如 strided 数组)拷贝成 C 行主序,这是 buffer 数据落地为 bytearray 的标准流程。
3.2 PyByteArray_FromStringAndSize:从原始内存构造
PyObject* PyByteArray_FromStringAndSize(const char *string, Py_ssize_t len)
用 string 及其长度 len 创建新的 bytearray;string 可以为 NULL(只分配不填充)。失败时返回 NULL 并设置异常。
实现细节见 bytearrayobject.c,有三个值得注意的点:
- 负长度直接触发 SystemError:
test_fromstringandsize 确认传入if (size < 0) { PyErr_SetString(PyExc_SystemError, "Negative size passed to PyByteArray_FromStringAndSize"); return NULL; }-1或PY_SSIZE_T_MIN时抛SystemError;而把NULL配PY_SSIZE_T_MAX这样的超大长度会因分配失败抛OverflowError。 - 零长度优化:
size == 0时复用全局空 bytes 常量,不发生实际内存分配(源码注释:“size=0 bytearray should not allocate space”); ob_exports必须显式清零:因为PyObject_New不做零初始化,若后续分配出错进入bytearray_dealloc,未初始化的ob_exports会导致间歇性测试失败(源码注释原样保留了这个动机)。
典型 C 扩展用法:
PyObject *ba = PyByteArray_FromStringAndSize("\x00\x01\x02", 3);
if (ba == NULL) return NULL; /* 异常已设置,向上层传播 */
/* ... 使用 ba ... */
Py_DECREF(ba);
四、连接与复制:PyByteArray_Concat
PyObject* PyByteArray_Concat(PyObject *a, PyObject *b)
连接 a 与 b,返回一个新的 bytearray(不修改原对象)。失败返回 NULL 并设置异常;两个参数只需支持 buffer 协议即可,不限于 bytearray。
实现(bytearrayobject.c)流程是:
- 用
PyObject_GetBuffer(a, &va, PyBUF_SIMPLE)分别取a、b的简单视图,任一步失败即报TypeError: can't concat ... to ...; - 溢出检查:
va.len > PyByteArray_SIZE_MAX - vb.len时直接PyErr_NoMemory(),避免va.len + vb.len溢出; PyByteArray_FromStringAndSize(NULL, va.len + vb.len)建结果,两段memcpy拷入,最后统一PyBuffer_Release。
上限宏 PyByteArray_SIZE_MAX 定义在 bytearrayobject.c,比 PY_SSIZE_T_MAX 少一个 bytes 对象头的空间,保证内部 bytes 容器可以容纳全部数据。test_concat 覆盖了 bytearray+bytes、bytes+bytearray、bytearray+bytearray 以及含内嵌 \0 字节的场景,并验证原对象 ba 未被修改。与 Python 层 ba + b 的区别在于:此处返回的总是 bytearray 子类之外的精确 bytearray。
五、读取大小与内容:PyByteArray_Size 与 PyByteArray_AsString
Py_ssize_t PyByteArray_Size(PyObject *bytearray)
检查 NULL 指针后返回 bytearray 的长度。实现(bytearrayobject.c)内部 assert 参数非空且为 bytearray,再转调快速宏 PyByteArray_GET_SIZE。
char* PyByteArray_AsString(PyObject *bytearray)
检查 NULL 后返回内容的 char 数组。返回数组总是额外附有一个末尾空字节,因此对长度为 n 的 bytearray 你可以安全地按 n + 1 字节读取——这是它能被当作 C 字符串处理的前提。test_asstring 验证:对 bytearray(b'') 读 1 字节得到 b'\0',对 bytearray(b'abc') 读 4 字节得到 b'abc\0',对内嵌 \0 的 b'abc\0def' 读 8 字节得到 b'abc\0def\0'。
文档给出的线程安全警告必须重视:在持有返回的 char* 期间修改该 bytearray 不是线程安全的。结合源码可知原因:返回的指针直接指向内部缓冲区(ob_start),并发线程的 resize 可能触发重分配使指针失效。安全做法是:读取前用 PyByteArray_Size 固定长度,读取后不再依赖该指针,或改用 PyObject_GetBuffer 获取带引用计数的导出视图。
六、改变缓冲区大小:PyByteArray_Resize(3.14 行为变更)
int PyByteArray_Resize(PyObject *bytearray, Py_ssize_t len)
把内部缓冲区 resize 到 len,失败返回 -1 并设置异常。文档中标注了 versionchanged 3.14:负的 len 现在会设置异常并返回 -1(此前语义不同),这一行为在 bytearray_resize_lock_held 中:
if (requested_size < 0) {
PyErr_Format(PyExc_ValueError,
"Can only resize to positive sizes, got %zd", requested_size);
return -1;
}
公开的 PyByteArray_Resize(bytearrayobject.c)在进入实现前先 Py_BEGIN_CRITICAL_SECTION(self) 获取对象临界区,保证与并发读写互斥;Python 层的 bytearray.resize(n) 方法走同一函数(bytearrayobject.c),且扩大时会 memset 新区域为零字节。
resize 的容量策略(bytearrayobject.c)与 list 的预分配思想一致,所有计算都用无符号数以避免整数溢出(源码注明对应 issue #22335):
| 场景 | 策略 |
|---|---|
| 目标尺寸 == 0 | 直接换回全局空 bytes 常量,释放整个缓冲区 |
| 轻微缩小(新尺寸 ≥ 原容量一半) | 不重分配,只把 ob_size 改小并补一个中间 \0 |
| 大幅缩小(新尺寸 < 原容量一半) | 精确收缩到目标尺寸 |
| 温和增大(≤ 1.125 倍) | 过度分配:alloc = size + (size >> 3) + (size < 9 ? 3 : 6),与 list_resize() 的公式一致 |
| 大幅增大(> 1.125 倍) | 精确分配目标尺寸 |
超过 PyByteArray_SIZE_MAX |
PyErr_NoMemory() 返回 -1 |
此外:
- 存在 buffer 导出时拒绝 resize:
_canresize检查ob_exports > 0则抛BufferError: Existing exports of data: object cannot be re-sized。这与PyByteArray_AsString/PyByteArray_AS_STRING的线程安全警告是同一根源——导出方(或裸指针持有方)看到的地址可能被换掉; - 失败回滚:若
_PyBytes_Resize失败,对象会被恢复到空状态(ob_bytes_object指回空 bytes 常量,size = alloc = 0),调用方拿到-1和一个合法的空 bytearray,不会留下半损坏对象。
扩展中的典型用法:
PyObject *ba = PyByteArray_FromStringAndSize("AB", 2);
if (ba != NULL) {
if (PyByteArray_Resize(ba, 10) < 0) {
Py_DECREF(ba);
ba = NULL; /* 异常已设置 */
} else {
memcpy(PyByteArray_AsString(ba) + 2, "CDEF", 4);
}
}
七、无检查的快速宏:PyByteArray_AS_STRING 与 PyByteArray_GET_SIZE
文档最后两组宏以速度换安全,不做指针检查,位于 cpython/bytearrayobject.h(仅完整 API 可用,不在 Stable/Limited API 中):
#define PyByteArray_AS_STRING(self) PyByteArray_AS_STRING(_PyObject_CAST(self)) /* 等价,无检查 */
static inline Py_ssize_t PyByteArray_GET_SIZE(PyObject *op) {
PyByteArrayObject *self = _PyByteArray_CAST(op);
#ifdef Py_GIL_DISABLED
return _Py_atomic_load_ssize_relaxed(&(_PyVarObject_CAST(self)->ob_size));
#else
return Py_SIZE(self);
#endif
}
PyByteArray_AS_STRING直接返回ob_start,对应PyByteArray_AsString去掉类型断言的版本;同样存在「持有期间不得并发修改」的线程安全约束;PyByteArray_GET_SIZE在构建于自由线程(无 GIL)模式(Py_GIL_DISABLED)下会以 relaxed 原子方式读取ob_size,这正是 bytearray 在 free-threaded 构建中长度读操作无锁可用的原因。
使用准则:确认对象是 bytearray(或 bytearray 子类)且在当前线程持有临界区/不并发修改时,用这两个宏省掉重复检查;面向公开入口的参数,仍应先用 PyByteArray_Check 再调用有检查的函数版本。
八、Buffer 协议约束与测试基线
文档在两处(FromObject、Concat)重复了一条关键约束:
若对象实现了 buffer 协议,则 bytearray 对象创建期间该 buffer 不得被修改。
从源码看,所有会改动内部缓冲区的公开入口(resize、切片赋值、take_bytes 等)都包在 Py_BEGIN_CRITICAL_SECTION 临界区中,并在执行期间临时抬高 ob_exports(见 _bytearray_with_buffer)防止并发导出视图失效。如果你的扩展同时持有 bytearray 的裸指针(来自 PyByteArray_AsString)又调用 PyByteArray_Resize,就会违反上述约束。
回归基线可参考:
- Lib/test/test_capi/test_bytearray.py:通过
_testlimitedcapi对Check、CheckExact、FromObject、FromStringAndSize、Size、AsString、Concat逐一断言正常路径、边界(内嵌\0、空对象、子类)与异常路径(负长度SystemError、超大长度OverflowError); - Modules/_testlimitedcapi/bytearray.c:这些测试包装器的实现,展示了 Limited API 下调用同一组
PyByteArray_*的标准方式。
九、实践清单
| 场景 | 推荐 API | 关键约束 |
|---|---|---|
| 判断 bytearray(含子类) | PyByteArray_Check |
总是成功 |
| 判断精确 bytearray | PyByteArray_CheckExact |
总是成功 |
| 从 buffer 对象构造 | PyByteArray_FromObject |
创建期间源 buffer 不可变;失败置异常 |
| 从裸内存构造 | PyByteArray_FromStringAndSize |
负长度 → SystemError;size=0 零分配 |
| 拼接两段数据 | PyByteArray_Concat |
参数支持 buffer 协议即可;结果恒为 bytearray |
| 取长度 | PyByteArray_Size / PyByteArray_GET_SIZE |
宏版不做检查 |
| 取 C 字符串视图 | PyByteArray_AsString / PyByteArray_AS_STRING |
末尾多一个 \0;持有期间不得并发修改 |
| 扩缩容量 | PyByteArray_Resize |
3.14 起负长度抛异常;有导出时抛 BufferError;失败时对象回滚为空 |
需要记住的三条铁律:① 所有构造/操作函数失败时返回 NULL 或 -1 并设置异常,不要吞掉后继续;② 裸 char* 视图只在「不 resize、无并发写」的前提下有效;③ buffer 导出计数 ob_exports 决定了对象能否被 resize,扩展中持有 memoryview/Py_buffer 时必须及时 PyBuffer_Release。掌握以上 API 与约束,你就能在 C 扩展中以零拷贝或最小拷贝的方式安全地读写 CPython 的 bytearray 对象。
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