首页
/ CPython bytearray C API 详解:PyByteArrayObject、PyByteArray_Type 与 PyByteArray_* 系列接口全解

CPython bytearray C API 详解:PyByteArrayObject、PyByteArray_Type 与 PyByteArray_* 系列接口全解

2026-09-05 16:12:41作者:翟江哲Frasier

在 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.cbytearray_setslice_linear 中可以看到,只有当缩减幅度很大时才会真正重分配;
  • ob_exports 是 buffer 导出的计数。每当通过 buffer 协议导出一块视图(如 memoryview),计数加一(bytearray_getbufferobj->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),因此它接受 bytesbytearray 子类、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,有三个值得注意的点:

  1. 负长度直接触发 SystemError:
    if (size < 0) {
        PyErr_SetString(PyExc_SystemError,
            "Negative size passed to PyByteArray_FromStringAndSize");
        return NULL;
    }
    
    test_fromstringandsize 确认传入 -1PY_SSIZE_T_MIN 时抛 SystemError;而把 NULLPY_SSIZE_T_MAX 这样的超大长度会因分配失败抛 OverflowError
  2. 零长度优化:size == 0 时复用全局空 bytes 常量,不发生实际内存分配(源码注释:“size=0 bytearray should not allocate space”);
  3. 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)

连接 ab,返回一个新的 bytearray(不修改原对象)。失败返回 NULL 并设置异常;两个参数只需支持 buffer 协议即可,不限于 bytearray。

实现(bytearrayobject.c)流程是:

  1. PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) 分别取 ab 的简单视图,任一步失败即报 TypeError: can't concat ... to ...;
  2. 溢出检查:va.len > PyByteArray_SIZE_MAX - vb.len 时直接 PyErr_NoMemory(),避免 va.len + vb.len 溢出;
  3. PyByteArray_FromStringAndSize(NULL, va.len + vb.len) 建结果,两段 memcpy 拷入,最后统一 PyBuffer_Release

上限宏 PyByteArray_SIZE_MAX 定义在 bytearrayobject.c,比 PY_SSIZE_T_MAX 少一个 bytes 对象头的空间,保证内部 bytes 容器可以容纳全部数据。test_concat 覆盖了 bytearray+bytesbytes+bytearraybytearray+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',对内嵌 \0b'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:通过 _testlimitedcapiCheckCheckExactFromObjectFromStringAndSizeSizeAsStringConcat 逐一断言正常路径、边界(内嵌 \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 对象。

登录后查看全文
热门项目推荐
相关项目推荐