PEP 782 – 添加 PyBytesWriter C API
- 作者:
- Victor Stinner <vstinner at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2025年3月27日
- Python 版本:
- 3.15
- 发布历史:
- 2025年2月18日
- 决议:
- 2025年9月11日
摘要
添加新的 PyBytesWriter
C API 以创建 bytes
对象。
软弃用 PyBytes_FromStringAndSize(NULL, size)
和 _PyBytes_Resize()
API。这些 API 将不可变的 bytes
对象视为可变对象。它们仍然可用并维护,不会发出弃用警告,但编写新代码时不再推荐使用。
基本原理
禁止创建不完整/不一致的对象
使用 PyBytes_FromStringAndSize(NULL, size)
和 _PyBytes_Resize()
创建 Python bytes
对象会将不可变的 bytes
对象视为可变对象。这违反了 bytes
对象不可变的原则。它还会创建一个不完整或“无效”的对象,因为字节未初始化。在 Python 中,bytes
对象应该始终完全初始化其字节。
低效的分配策略
当创建字节字符串且输出大小未知时,一种策略是分配一个短缓冲区,并在每次需要更大写入时将其扩展(到精确大小)。
这种策略效率低下,因为它需要多次扩大缓冲区。在第一次需要更大写入时,过度分配缓冲区会更有效。它减少了昂贵的 realloc()
操作次数,这可能意味着内存复制。
规范
API
-
type PyBytesWriter
- 由
PyBytesWriter_Create()
创建的 Pythonbytes
写入器实例。该实例必须通过
PyBytesWriter_Finish()
或PyBytesWriter_Discard()
销毁。
创建、完成、丢弃
-
PyBytesWriter *PyBytesWriter_Create(Py_ssize_t size)
- 创建
PyBytesWriter
以写入 size 字节。如果 size 大于零,则分配 size 字节,并将写入器大小设置为 size。调用者负责使用
PyBytesWriter_GetData()
写入 size 字节。出错时,设置异常并返回 NULL。
size 必须为正数或零。
-
PyObject *PyBytesWriter_Finish(PyBytesWriter *writer)
- 完成由
PyBytesWriter_Create()
创建的PyBytesWriter
。成功时,返回一个 Python
bytes
对象。出错时,设置异常并返回NULL
。无论如何,调用后写入器实例都无效。
-
PyObject *PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size)
- 类似于
PyBytesWriter_Finish()
,但在创建bytes
对象之前将写入器调整为 size 字节。
-
PyObject *PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf)
- 类似于
PyBytesWriter_Finish()
,但在创建bytes
对象之前使用 buf 指针调整写入器大小。如果 buf 指针超出内部缓冲区边界,则设置异常并返回
NULL
。函数伪代码
Py_ssize_t size = (char*)buf - (char*)PyBytesWriter_GetData(writer); return PyBytesWriter_FinishWithSize(writer, size);
-
void PyBytesWriter_Discard(PyBytesWriter *writer)
- 丢弃由
PyBytesWriter_Create()
创建的PyBytesWriter
。如果 writer 是
NULL
,则不执行任何操作。调用后写入器实例无效。
高级 API
-
int PyBytesWriter_WriteBytes(PyBytesWriter *writer, const void *bytes, Py_ssize_t size)
- 将 writer 内部缓冲区增长 size 字节,在 writer 末尾写入 size 字节的 bytes,并将 size 添加到 writer 大小。
如果 size 等于
-1
,则调用strlen(bytes)
获取字符串长度。成功时,返回
0
。出错时,设置异常并返回-1
。
-
int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...)
- 类似于
PyBytes_FromFormat()
,但直接在写入器末尾写入输出。按需增长写入器内部缓冲区。然后将写入大小添加到写入器大小。成功时,返回
0
。出错时,设置异常并返回-1
。
获取器
-
Py_ssize_t PyBytesWriter_GetSize(PyBytesWriter *writer)
- 获取写入器大小。
-
void *PyBytesWriter_GetData(PyBytesWriter *writer)
- 获取写入器数据:内部缓冲区的起始位置。
该指针在对 writer 调用
PyBytesWriter_Finish()
或PyBytesWriter_Discard()
之前有效。
低级 API
-
int PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size)
- 将写入器大小调整为 size 字节。它可用于扩大或缩小写入器。
新分配的字节未初始化。
成功时,返回
0
。出错时,设置异常并返回-1
。size 必须为正数或零。
-
int PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t grow)
- 通过在当前写入器大小上添加 grow 字节来调整写入器大小。
新分配的字节未初始化。
成功时,返回
0
。出错时,设置异常并返回-1
。size 可以是负数来缩小写入器。
-
void *PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf)
- 类似于
PyBytesWriter_Grow()
,但同时更新 buf 指针。如果内部缓冲区在内存中移动,则 buf 指针也会移动。buf 在内部缓冲区内的相对位置保持不变。
出错时,设置异常并返回
NULL
。buf 不能是
NULL
。函数伪代码
Py_ssize_t pos = (char*)buf - (char*)PyBytesWriter_GetData(writer); if (PyBytesWriter_Grow(writer, size) < 0) { return NULL; } return (char*)PyBytesWriter_GetData(writer) + pos;
过度分配
PyBytesWriter_Resize()
和 PyBytesWriter_Grow()
会过度分配内部缓冲区,以减少 realloc()
调用的次数,从而减少内存复制。
PyBytesWriter_Finish()
会修剪过度分配:它在创建最终 bytes
对象时将内部缓冲区缩小到精确大小。
线程安全
该 API 不是线程安全的:一个写入器应仅由单个线程同时使用。
软弃用
软弃用 PyBytes_FromStringAndSize(NULL, size)
和 _PyBytes_Resize()
API。这些 API 将不可变的 bytes
对象视为可变对象。它们仍然可用并维护,不会发出弃用警告,但编写新代码时不再推荐使用。
PyBytes_FromStringAndSize(str, size)
未软弃用。只有带有 NULL
str 的调用才被软弃用。
示例
高级 API
创建字节字符串 b"Hello World!"
PyObject* hello_world(void)
{
PyBytesWriter *writer = PyBytesWriter_Create(0);
if (writer == NULL) {
goto error;
}
if (PyBytesWriter_WriteBytes(writer, "Hello", -1) < 0) {
goto error;
}
if (PyBytesWriter_Format(writer, " %s!", "World") < 0) {
goto error;
}
return PyBytesWriter_Finish(writer);
error:
PyBytesWriter_Discard(writer);
return NULL;
}
创建字节字符串“abc”
创建固定大小为 3 字节的字节字符串 b"abc"
的示例
PyObject* create_abc(void)
{
PyBytesWriter *writer = PyBytesWriter_Create(3);
if (writer == NULL) {
return NULL;
}
char *str = PyBytesWriter_GetData(writer);
memcpy(str, "abc", 3);
return PyBytesWriter_Finish(writer);
}
GrowAndUpdatePointer()
示例
使用指针写入字节并跟踪写入大小的示例。
创建字节字符串 b"Hello World"
PyObject* grow_example(void)
{
// Allocate 10 bytes
PyBytesWriter *writer = PyBytesWriter_Create(10);
if (writer == NULL) {
return NULL;
}
// Write some bytes
char *buf = PyBytesWriter_GetData(writer);
memcpy(buf, "Hello ", strlen("Hello "));
buf += strlen("Hello ");
// Allocate 10 more bytes
buf = PyBytesWriter_GrowAndUpdatePointer(writer, 10, buf);
if (buf == NULL) {
PyBytesWriter_Discard(writer);
return NULL;
}
// Write more bytes
memcpy(buf, "World", strlen("World"));
buf += strlen("World");
// Truncate the string at 'buf' position
// and create a bytes object
return PyBytesWriter_FinishWithPointer(writer, buf);
}
更新 PyBytes_FromStringAndSize()
代码
使用软弃用的 PyBytes_FromStringAndSize(NULL, size)
API 的代码示例
PyObject *result = PyBytes_FromStringAndSize(NULL, num_bytes);
if (result == NULL) {
return NULL;
}
if (copy_bytes(PyBytes_AS_STRING(result), start, num_bytes) < 0) {
Py_CLEAR(result);
}
return result;
现在可以更新为
PyBytesWriter *writer = PyBytesWriter_Create(num_bytes);
if (writer == NULL) {
return NULL;
}
if (copy_bytes(PyBytesWriter_GetData(writer), start, num_bytes) < 0) {
PyBytesWriter_Discard(writer);
return NULL;
}
return PyBytesWriter_Finish(writer);
更新 _PyBytes_Resize()
代码
使用软弃用的 _PyBytes_Resize()
API 的代码示例
PyObject *v = PyBytes_FromStringAndSize(NULL, size);
if (v == NULL) {
return NULL;
}
char *p = PyBytes_AS_STRING(v);
// ... fill bytes into 'p' ...
if (_PyBytes_Resize(&v, (p - PyBytes_AS_STRING(v)))) {
return NULL;
}
return v;
现在可以更新为
PyBytesWriter *writer = PyBytesWriter_Create(size);
if (writer == NULL) {
return NULL;
}
char *p = PyBytesWriter_GetData(writer);
// ... fill bytes into 'p' ...
return PyBytesWriter_FinishWithPointer(writer, p);
参考实现
CPython 参考实现说明,不属于规范
- 实现内部分配一个
bytes
对象,因此PyBytesWriter_Finish()
只返回对象而无需复制内存。 - 对于不超过 256 字节的字符串,使用小的内部原始字节缓冲区。这避免了调整
bytes
对象的大小,因为这效率低下。最后,PyBytesWriter_Finish()
从这个小缓冲区创建bytes
对象。 - 使用空闲列表来降低在堆内存上分配
PyBytesWriter
的成本。
向后兼容性
对向后兼容性没有影响,只添加了新的 API。
PyBytes_FromStringAndSize(NULL, size)
和 _PyBytes_Resize()
API 已被软弃用。这些函数在使用时不会发出新的警告,也不打算移除。
先前的讨论
- 2025 年 3 月:第三次公开 API 尝试,使用大小而不是指针
- 2025 年 2 月:第二次公开 API 尝试
- 2024 年 7 月:第一次公开 API 尝试
- C API 工作组决定:添加 PyBytes_Writer() API (2024 年 8 月)
- 拉取请求 gh-121726:第一次公开 API 尝试(2024 年 7 月)
- 2016 年 3 月:CPython 中用于生成字符串的快速 _PyAccu、_PyUnicodeWriter 和 _PyBytesWriter API:关于原始私有
_PyBytesWriter
C API 的文章。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0782.rst