news 2026/9/7 5:30:39

CPython bytes 对象 C API 深度解析:PyBytes_* 与 PyBytesWriter 新 API(3.15)全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPython bytes 对象 C API 深度解析:PyBytes_* 与 PyBytesWriter 新 API(3.15)全解

CPython bytes 对象 C API 深度解析:PyBytes_* 与 PyBytesWriter 新 API(3.15)全解

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

本文以 CPython 官方 C API 文档 Doc/c-api/bytes.rst 为主体,结合仓库中的头文件与源码实现,系统讲解 bytes 对象在 C 扩展层的完整 API 面:类型判定、创建、读取、拼接、格式化,以及 3.15 新增的PyBytesWriter增量构建 API。读完本文,你可以为 C 扩展编写安全、高效、符合当前 CPython 规范的 bytes 处理代码,并理解这些 API 在 Objects/bytesobject.c 中的底层行为。

需要说明的适用前提:本仓库为 CPython main 分支源码,其中PyBytesWriter标注为 3.15 新增,PyBytes_Join标注为 3.14 新增,而PyBytes_FromStringAndSize(NULL, len)_PyBytes_Resize已在 3.15 被软弃用(soft-deprecated)。如果你的扩展面向更旧版本运行,请留意这些版本边界。

bytes 类型在 C 层的表示

C API 文档开篇即给出一个统一约定:这些函数在期望 bytes 参数时,如果收到非 bytes 参数会抛出TypeError(部分函数则不检查类型,后文会逐一标注)。

PyBytesObject 布局

公开文档将PyBytesObject描述为“PyObject的一个子类型,代表 Python 的 bytes 对象”。在源码层面,其完整定义位于 Include/cpython/bytesobject.h:

typedef struct { PyObject_VAR_HEAD Py_DEPRECATED(3.11) Py_hash_t ob_shash; char ob_sval[1]; /* Invariants: * ob_sval contains space for 'ob_size+1' elements. * ob_sval[ob_size] == 0. * ob_shash is the hash of the byte string or -1 if not computed yet. */ } PyBytesObject;

三个关键事实:

  • 尾部保证 null 终止:缓冲区实际分配ob_size + 1字节,最后一个字节恒为'\0'。这正是PyBytes_AsString能返回char*且可当 C 字符串使用的依据——即使内容中间含\0也不会截断语义,长度以ob_size为准;
  • 不可变:bytes 是 immutable 类型,公开 API 从不提供原地修改入口(唯一的“原地”能力_PyBytes_Resize是私有 API 且已被软弃用,见后文);
  • 内部细节受Py_LIMITED_API保护:Include/bytesobject.h 仅在未启用稳定 ABI 时才包含cpython/bytesobject.h。也就是说,编写 stable ABI 扩展时你只能拿到PyBytes_AsStringAndSize这类函数接口,拿不到结构体字段——这是 CPython 有意为之的设计。

PyBytes_Type是 bytes 类型的PyTypeObject实例,与 Python 层的bytes类是同一个对象,声明见 Include/bytesobject.h。

类型判定:PyBytes_Check 与 PyBytes_CheckExact

int PyBytes_Check(PyObject *o) int PyBytes_CheckExact(PyObject *o)
  • PyBytes_Checko是 bytes 或 bytes子类型的实例时返回真;
  • PyBytes_CheckExact:仅当o恰好是 bytes 类型(不含子类)时返回真。

两者都“总是成功”,不会失败。实现上它们是宏,见 Include/bytesobject.h:

#define PyBytes_Check(op) \ PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_BYTES_SUBCLASS) #define PyBytes_CheckExact(op) Py_IS_TYPE((op), &PyBytes_Type)

从源码结构看,PyBytes_Check依赖Py_TPFLAGS_BYTES_SUBCLASS标志位做快速子类判定,这与 list、dict 等可变容器的判定方式一致。经验法则:如果你打算调用依赖具体布局的内部接口,用CheckExact;如果只是需要“按 buffer 协议消费”,用Check即可。

创建 bytes 对象

PyBytes_FromString 与 PyBytes_FromStringAndSize

PyObject* PyBytes_FromString(const char *v) PyObject* PyBytes_FromStringAndSize(const char *v, Py_ssize_t len)
  • 成功返回持有新数据拷贝的 bytes 对象,失败返回NULL
  • PyBytes_FromString要求v不得为 NULL(不会检查,传 NULL 是未定义行为),按 null 终止串拷贝;
  • PyBytes_FromStringAndSize显式指定长度,因此数据可以包含内嵌\0,且不必 null 终止——这使得“从大缓冲区截取子串”这类操作天然安全。

文档给出的重要警告是:当vNULL时,对象的内存是未初始化的。Objects/bytesobject.c 中对该行为的注释写得更直白:此时分配size+1字节(末字节置\0),留给你自己填充数据。这正是历史代码中“先占坑后写数据”模式的来源,也正是 3.15 弃用它的动机:

3.15 软弃用PyBytes_FromStringAndSize(NULL, len)的用法建议改用PyBytesWriterAPI。

实现细节值得注意:长度为 0 时返回的是不可回收的空 bytes 单例,单字节结果同样命中单例池——Objects/bytesobject.c 的_PyBytes_FromSizesize == 0时直接返回bytes_get_empty()单例。

PyBytes_FromFormat:printf 风格的二进制拼装

PyObject* PyBytes_FromFormat(const char *format, ...) PyObject* PyBytes_FromFormatV(const char *format, va_list vargs)

PyBytes_FromFormat接受 printf 风格格式串,一次计算并填充结果,免去“先算长度、再分配、再格式化”三步走;FromFormatVva_list变体,适合封装 variadic 函数时转发参数。文档强调:变参必须是 C 类型且与格式字符精确对应。官方支持的格式字符表如下(完整继承自 Doc/c-api/bytes.rst):

格式字符参数类型说明
%%n/a字面量%
%cint单个字节,用 Cint表示
%dint等价于printf("%d")(见注 1)
%uunsigned int等价于printf("%u")(见注 1)
%ldlong等价于printf("%ld")(见注 1)
%luunsigned long等价于printf("%lu")(见注 1)
%zdPy_ssize_t等价于printf("%zd")(见注 1)
%zusize_t等价于printf("%zu")(见注 1)
%iint等价于printf("%i")(见注 1)
%xint等价于printf("%x")(见注 1)
%sconst char*null 终止的 C 字符数组
%pconst void*指针的十六进制表示;与printf("%p")基本一致,但保证以字面量0x开头

注 1:对所有整型说明符(d/u/ld/lu/zd/zu/i/x),即使指定了精度,0转换标志仍然生效。

另一个行为细节:遇到无法识别的格式字符时,格式串剩余部分会被原样拷贝进结果,多余参数被丢弃——它是“宽容”的而非“报错”的。头文件中还标注了Py_GCC_ATTRIBUTE((format(printf, ...)))(见 Include/bytesobject.h),即编译器会像检查printf一样帮你检查格式串与参数的匹配性,写扩展时应保留这个属性。

典型用途是协议封包类代码:

// 构造一个 3 字节的帧头:opcode、类型、2 字节长度 PyObject * make_frame_header(int opcode, int type, Py_ssize_t len) { return PyBytes_FromFormat("%c%c%zd", opcode, type, len); }

PyBytes_FromObject:经由 buffer 协议转换

PyObject* PyBytes_FromObject(PyObject *o)

返回实现 buffer 协议的对象的 bytes 表示。文档特别提醒:创建期间源 buffer 不得被修改——这是 buffer 协议对象的通用约束。buffer 协议的完整定义可参阅 Doc/c-api/buffer.rst。

读取长度与内容

长度:PyBytes_Size 与 PyBytes_GET_SIZE

Py_ssize_t PyBytes_Size(PyObject *o) Py_ssize_t PyBytes_GET_SIZE(PyObject *o)

PyBytes_Size做类型检查(失败时置错);PyBytes_GET_SIZE是“无错误检查”的快速宏,实现见 Include/cpython/bytesobject.h:它直接读取Py_SIZE(self)。两者仅在非稳定 ABI 下可拿到底层宏,但函数形式始终可用。

内容指针:PyBytes_AsString、PyBytes_AS_STRING

char* PyBytes_AsString(PyObject *o) char* PyBytes_AS_STRING(PyObject *o)

PyBytes_AsString返回内部缓冲区指针,该缓冲区共len(o) + 1字节、末尾恒为 null。使用约束(文档原文语义):

  • 数据不得修改——除非该对象是刚用PyBytes_FromStringAndSize(NULL, size)创建的占位对象;
  • 指针不得释放,它属于对象自身;
  • o不是 bytes 时返回NULL并抛出TypeError

PyBytes_AS_STRING跳过类型检查,直接强制转换读取(Include/cpython/bytesobject.h),只应在已确认为 bytes 的路径上使用。

带长度出口:PyBytes_AsStringAndSize

int PyBytes_AsStringAndSize(PyObject *obj, char **buffer, Py_ssize_t *length)

通过出参返回 null 终止的内容指针与长度,成功返回0。关键行为:

  • lengthNULL,表示调用方不允许内嵌\0;一旦检测到内嵌空字节,函数返回-1并抛出ValueError
  • 返回的 buffer 指向内部缓冲区(末尾多出的 null 字节不计入length),修改与释放约束同上;
  • 对象非 bytes 时返回-1并抛TypeError

3.5 版本变更:更早版本在内嵌 null 字节时抛的是TypeError,3.5 起改为ValueError——如果你的扩展对返回值和异常做联动处理,注意这个语义。这是 C 层读取二进制数据的推荐入口:它同时给出指针和长度,避免对含\0数据误用strlen

拼接、合并与调整大小

PyBytes_Concat 与 PyBytes_ConcatAndDel

void PyBytes_Concat(PyObject **bytes, PyObject *newpart) void PyBytes_ConcatAndDel(PyObject **bytes, PyObject *newpart)

两者都在*bytes处构造“旧内容 +newpart”的新 bytes 对象。引用计数语义需要仔细对待:

  • 调用者持有新对象的引用
  • *bytes旧值的引用被“偷走”(stolen),即不再释放;
  • 若新对象创建失败,旧引用依然被偷走,*bytes被置为NULL,异常被置位;
  • PyBytes_ConcatAndDel额外会递减newpart的引用计数(强引用被释放)。

newpart实现 buffer 协议,创建期间其 buffer 不得被修改。

PyBytes_Join(3.14 新增)

PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable)

与 Python 层sep.join(iterable)语义相同:

  • sep必须是 bytes 对象——注意它与PyUnicode_Join的差异:后者接受NULL分隔符并视为空格,而PyBytes_Join不接受NULL分隔符;
  • iterable必须是可迭代对象,且产出的元素必须实现 buffer 协议;
  • 成功返回新 bytes,失败置异常并返回NULL

该 API 在 Include/cpython/bytesobject.h 中声明,同时保留了Py_DEPRECATED(3.14)_PyBytes_Join别名转发到新名称。C 解释器自身也在用它拼装字节串,例如 Modules/_sre/sre.c 和 Modules/_io/bufferedio.c 中均可看到PyBytes_Join的调用,Lib/test/test_capi/test_bytes.py 则提供了它的 C API 测试覆盖。

_PyBytes_Resize:私有 API 与弃用方向

int _PyBytes_Resize(PyObject **bytes, Py_ssize_t newsize)

这是以_Py前缀暴露的私有接口:把*bytes指向的对象调整到新长度,可理解成“创建新对象并销毁旧对象,只是更高效”。成功时*bytes保存调整后的对象(地址可能已变);重分配失败时原对象被释放、*bytesNULL、设置MemoryError并返回-1

3.15 软弃用:官方建议改用PyBytesWriter。带下划线的私有前缀本身就表明它不属于稳定 ABI,扩展代码不应继续依赖它。

表示与转义:PyBytes_Repr 与 PyBytes_DecodeEscape

PyBytes_Repr

PyObject *PyBytes_Repr(PyObject *bytes, int smartquotes)

获取 bytes 的字符串表示,当前用于实现 Python 层bytes.__repr__。注意它不做类型检查:传入非 bytes 或NULL是未定义行为。smartquotes为真时,若内容含单引号则改用双引号包裹:字节串b"'Python'"smartquotes为真时显示为b"'Python'",为假时显示为b"\'Python\'"。成功返回表示串的str对象(强引用),失败返回NULL并置异常。

PyBytes_DecodeEscape

PyObject *PyBytes_DecodeEscape(const char *s, Py_ssize_t len, const char *errors, Py_ssize_t unicode, const char *recode_encoding)

对反斜杠转义串s做反转义,s不得为NULLlen必须为其大小。errors"strict""replace""ignore"之一,传NULL时默认"strict"。成功返回反转义后的 bytes 对象(强引用),失败返回NULL并置异常。3.9 变更unicoderecode_encoding两个参数已不再使用,仅为保持 ABI 兼容而保留。

PyBytesWriter:3.15 新增的增量构建 API

PyBytesWriter是本文档最值得关注的部分——它是 3.15 引入的公开 API,用于构建 Python bytes 对象,同时接过了PyBytes_FromStringAndSize(NULL, len)(预分配未知内容)和_PyBytes_Resize(增量调整)两个被软弃用场景的职责。

生命周期:Create / Finish / Discard

PyBytesWriter* PyBytesWriter_Create(Py_ssize_t size) PyObject* PyBytesWriter_Finish(PyBytesWriter *writer) PyObject* PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size) PyObject* PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf) void PyBytesWriter_Discard(PyBytesWriter *writer)

生命周期规则非常严格:

  • PyBytesWriter_Create(size):创建一个 writer。size必须 ≥ 0;若size > 0精确分配size字节并置 writer 大小为size(注意:此函数不做过量分配),此后调用者负责通过PyBytesWriter_GetData把这size字节写完;出错返回NULL并置异常;
  • PyBytesWriter_Finish:把 writer 收尾为 Python bytes 对象。无论成败,调用之后 writer 实例即失效,不能再调用任何 API
  • PyBytesWriter_FinishWithSize(writer, size):与Finish类似,但先按size调整大小再产出对象。典型场景是用GetData拿到指针做二进制写操作(如fwrite/read)后,实际写入量可能小于申请量,用结束指针差值确定真实长度;
  • PyBytesWriter_FinishWithPointer(writer, buf):用指针buf定位结束位置,伪代码为size = buf - PyBytesWriter_GetData(writer),然后等价于FinishWithSize。若buf不在内部缓冲区范围内,置异常并返回NULL(源码实现抛出ValueError: "invalid end pointer",见 Objects/bytesobject.c);
  • PyBytesWriter_Discard(writer):错误路径上丢弃 writer;writerNULL时什么都不做;调用后实例同样失效。

一个完整的工作流示例(注意错误路径必须Discard):

PyObject * encode_record(const char *tag, Py_ssize_t n) { PyBytesWriter *writer = PyBytesWriter_Create(32); // 预估容量 if (writer == NULL) { return NULL; // 异常已置 } if (PyBytesWriter_WriteBytes(writer, tag, -1) < 0) { // -1 表示按 strlen 取长 PyBytesWriter_Discard(writer); return NULL; } if (PyBytesWriter_Format(writer, "|%zd", n) < 0) { PyBytesWriter_Discard(writer); return NULL; } PyObject *result = PyBytesWriter_Finish(writer); // 成败后 writer 均失效 return result; }

PyBytesWriter还有一条明确的红线:不线程安全——同一时刻只能由一个线程使用。这与 Doc/data/threadsafety.dat 中对 C API 线程安全属性的逐条登记机制相一致。

高层写入:WriteBytes 与 Format

int PyBytesWriter_WriteBytes(PyBytesWriter *writer, const void *bytes, Py_ssize_t size) int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...)
  • WriteBytes:把 writer 内部缓冲区增长size字节,将bytessize字节拷到 writer 末端,并累加 writer 大小。size == -1时以strlen(bytes)计算长度。成功返回0,失败置异常返回-1
  • Format:与PyBytes_FromFormat相同支持集,但输出直接写到 writer 末端,按需增长缓冲区。

实现见 Objects/bytesobject.c:WriteBytesGrowmemcpyFormat则先按格式串长度Grow预留空间,写完后用PyBytesWriter_Resize收缩回真实输出长度——这解释了为什么格式串可以包含占位符而不会撑大最终对象。

访问器:GetData 与 GetSize

void* PyBytesWriter_GetData(PyBytesWriter *writer) Py_ssize_t PyBytesWriter_GetSize(PyBytesWriter *writer)

GetData返回内部缓冲区起点指针;指针保持有效,直到对该 writer 调用了GetData/GetSize之外的任何 API。两者都不能失败。典型用法是拿到指针后通过FinishWithPointer报告写入终点,或配合GrowAndUpdatePointer维护“当前位置”游标。

低层控制:Resize、Grow、GrowAndUpdatePointer

int PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size) int PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t grow) void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf)
  • Resize:把 writer 调整到size字节(可放大也可缩小),新分配的字节保持未初始化。该函数会按策略过量分配,以获得多次 resize 下的摊还性能;
  • Grow:在当前大小基础上增加grow字节;grow可为负以收缩(但收缩后大小不能为负,否则抛ValueError);同样过量分配;
  • GrowAndUpdatePointer:等价于Grow但会同步更新游标指针buf——若扩容导致内部缓冲区整体搬迁,buf也随之移动,且其相对内部缓冲区的偏移保持不变;出错置异常返回NULLbuf不得为NULL。文档给出的伪代码即“记录偏移 → Grow → 重新加偏移”。

源码透视:小对象缓冲、过量分配与单例回收

文档描述了“通常过量分配”这类行为,而 Objects/bytesobject.c 和 Include/internal/pycore_bytesobject.h 给出了完整机制:

  1. 256 字节栈上小缓冲(SBO)PyBytesWriter内部结构含char small_buffer[256]。只要数据不超过 256 字节,writer 就完全不触发堆分配——这是绝大多数“拼个短头”场景的快路径;

  2. 按平台调参的过量分配系数

    #ifdef MS_WINDOWS /* On Windows, overallocate by 50% is the best factor */ # define OVERALLOCATE_FACTOR 2 #else /* On Linux, overallocate by 25% is the best factor */ # define OVERALLOCATE_FACTOR 4 #endif

    即 Linux 上每次 resize 追加 25% 容量、Windows 上追加 50%,注释表明这是实测调参结果(见 Objects/bytesobject.c);

  3. writer 结构体走 freelistbyteswriter_create通过_Py_FREELIST_POP_MEM(bytes_writers)复用结构体内存,Discard时归还——高频短生命周期 writer 的分配开销被摊平;

  4. 单例回填PyBytesWriter_FinishWithSize在结果为空串或单字节时,会替换为全局空串单例或字节单例池中的对象(Objects/bytesobject.c),与PyBytes_FromStringAndSize的单例行为保持一致,从而保证小 bytes 的内存与is语义稳定。

从源码结构看,Create阶段“不做过量分配”(resize标志为 0)、而Grow/Resize阶段“做过量分配”的设计,让调用者可以用预估容量精确起步,再由写入过程按需弹性扩张——这正是它替代_PyBytes_Resize手动循环的原因。

实践建议与验证途径

结合文档中的弃用标记,面向当前 CPython 的代码组织建议如下:

场景推荐 API避免
从已知 C 字符串构造PyBytes_FromString/FromFormat
长度已知、内容未知,逐段填充PyBytesWriter_Create+GetData/WriteBytes+FinishPyBytes_FromStringAndSize(NULL, n)(3.15 起软弃用)
反复追加、无法预估总长PyBytesWriter+Grow/WriteBytes_PyBytes_Resize(私有且 3.15 起软弃用)、重复PyBytes_Concat
一次性拼接可迭代序列PyBytes_Join手工循环Concat
读取内容PyBytes_AsStringAndSize(指针+长度)对返回值直接strlen
需要精确类型判断PyBytes_CheckExact(内部布局相关路径)/PyBytes_Check(消费路径)

所有成功/失败路径遵循统一纪律:返回NULL/-1即异常已置位,错误分支上必须PyBytesWriter_Discard收尾,Finish/FinishWithSize/FinishWithPointer之后不得再触碰 writer。

验证与深入阅读路径(均为本仓库内文件):

  • 实现主体:Objects/bytesobject.c(含PyBytesWriter全量实现,自 Objects/bytesobject.c#L3590 起);
  • 公共头文件:Include/bytesobject.h、Include/cpython/bytesobject.h(稳定 ABI 下不可见);
  • 内部结构体与内联辅助:Include/internal/pycore_bytesobject.h;
  • C API 测试:Lib/test/test_capi/test_bytes.py 与配套 C 扩展 Modules/_testcapi/bytes.c,覆盖了PyBytesWriter_Create、单例行为、内部_PyBytesWriter_CreateByteArray等细节;
  • 版本变更说明:Doc/whatsnew/3.14.rst 记录了PyBytes_Join的引入;PyBytes_FromStringAndSize(NULL, n)_PyBytes_Resize的后续移除计划见 Doc/deprecations/c-api-pending-removal-in-3.18.rst。

小结

CPython 的 bytes C API 以“不可变 + 尾 null + 长度显式”为核心设计:创建类函数负责把 C 内存安全地提升为 Python 对象,读取类函数在“可当 C 字符串用”与“二进制安全”之间提供了显式开关(As*系列是否带长度参数),而 3.15 的PyBytesWriter用一套带严格生命周期语义的增量构建 API,统一收编了原先靠“NULL 占位 + 私有 Resize”拼凑的二进制拼装模式。理解PyBytesWriter的 SBO 快路径、平台相关的过量分配系数和单例回填(Objects/bytesobject.c#L3590-L3794),能让你在新扩展中既写出符合弃用方向的代码,也清楚其底层性能特征从何而来。

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 5:26:37

动力电池CCS设计全解析:从电芯连接到采样总成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:26:02

网页模板HTML源码下载后怎么改?从拆解到上线的完整指南

简介&#xff1a;这是一套面向网页开发初学者的基础HTML模板资源&#xff0c;适合希望理解静态网页结构、样式与交互配合方式的读者。压缩包共9个文件&#xff0c;包含一个主HTML页面、配套CSS样式表、JavaScript脚本、jQuery scrollTo滚动插件及相关图片资源。CSS用于定义页面…

作者头像 李华
网站建设 2026/9/7 5:24:46

小米设备接入 Home Assistant 终极指南:ha_xiaomi_home 完整上手

小米设备接入 Home Assistant 终极指南&#xff1a;ha_xiaomi_home 完整上手 【免费下载链接】ha_xiaomi_home Xiaomi Home Integration for Home Assistant 项目地址: https://gitcode.com/GitHub_Trending/ha/ha_xiaomi_home 在 Home Assistant 面板上点开关&#xff…

作者头像 李华