cpython 源码分析 PyByteArrayObject(bytearray)
PyByteArrayObject
本文参考的是 3.8.0a0 版本的代码,详见 cpython 源码分析 基本篇
我们都知道在 python3 中,bytes 和 unicode 两种不同的对象被严格的区分开来,byte 就是表示字节数组,并且是不可变的字节数组,你不能改变 bytes 和 unicode 中的任意一个元素
a = b"aabbcc"
# TypeError: 'bytes' object does not support item assignment
a[0] = "1"
b = "aabbcc"
# TypeError: 'str' object does not support item assignment
b[0] = "1"
# AttributeError: 'bytes' object has no attribute 'encode'
a.encode("utf8")
# 'aabbcc'
a.decode("utf8")
# b'aabbcc'
b.encode("utf8")
# AttributeError: 'str' object has no attribute 'decode'
b.decode("utf8")
当我们需要改变一个 bytes 对象时,就需要用到字节数组
a = bytearray(b"aabbcc")
a[0:2] = b"xx"
a # bytearray(b'xxbbcc')
那么我们来看下 PyByteArrayObject 的 memory layout
PyObject_VAR_HEAD 表示这个对象是一个容器对象,同时含有 ob_base 和 ob_size
ob_alloc 表示这个 bytearray 占用的字节数
ob_bytes 表示这个 bytearray 开始的位置(实际位置
ob_start 表示当前标记的这个 bytearray 开始的位置(逻辑位置
ob_exports 记录了当前的 bytearray 被多少个外部对象共享/引用
通过下面的函数可以知道 ob_alloc,ob_bytes, ob_start 表示的内容
/* Objects/bytearrayobject.c
127 - 170 行
*/
PyObject *
PyByteArray_FromStringAndSize(const char *bytes, Py_ssize_t size)
{
// 根据传入的 bytes 和 size 生成一个 PyByteArrayObject 返回给调用者
PyByteArrayObject *new; // 创建一个叫做 new 的指向 PyByteArrayObject 的指针
Py_ssize_t alloc; // alloc 用来表示需要向操作系统申请的空间
if (size < 0) {
/* 不允许 size < 0 的调用 */
PyErr_SetString(PyExc_SystemError,
"Negative size passed to PyByteArray_FromStringAndSize");
return NULL;
}
/* 如果 size 为当前类型的最大值,当设置 alloc 为 size + 1 时会产生溢出,防止溢出 */
if (size == PY_SSIZE_T_MAX) {
return PyErr_NoMemory();
}
/* 为 new 分配 PyByteArrayObject 所需的基本空间 */
new = PyObject_New(PyByteArrayObject, &PyByteArray_Type);
if (new == NULL)
return NULL;
if (size == 0) {
new->ob_bytes = NULL;
alloc = 0;
}
else {
/* alloc 设置为 size +1 */
alloc = size + 1;
/* 向操作系统申请 alloc 字节的空间,并赋值到 new->ob_bytes 上 */
new->ob_bytes = PyObject_Malloc(alloc);
if (new->ob_bytes == NULL) {
Py_DECREF(new);
return PyErr_NoMemory();
}
if (bytes != NULL && size > 0)
/* 如果申请成功,则从 bytes 指针位置拷贝 size 大小的字节到 new->ob_bytes 位置上 */
memcpy(new->ob_bytes, bytes, size);
new->ob_bytes[size] = '\0'; /* Trailing null byte */
}
Py_SIZE(new) = size; /* 把 PyObject_VAR_HEAD-> ob_size 设置成 size */
new->ob_alloc = alloc;
new->ob_start = new->ob_bytes; /* ob_start 和 ob_bytes 都指向初始位置 */
new->ob_exports = 0; /*ob_exports 为 9 */
return (PyObject *)new;
}
ob_exports
那么 ob_exports 用来表示什么呢? 查阅下面的参考资料可以找到一些使用场景,比如我需要读入一个非常大的文件,或者直接从 socket 读取一个很长的二进制流,首先想到的方法是这么做
connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connection.connect((host_to_connect, 80))
r = connection.recv(2 ** 32) # 假设我的系统内核允许一次填满这么多 bytes
# 如果我想更改其中的某几个字节,再写回其他地方
# 会出现最开始的问题: # TypeError: 'bytes' object does not support item assignment
r[:2] = b"xx"
# 这个时候我只能调用 bytearray 新建一个 PyByteArrayObject
r2 = bytearray(r)
# 此时,我的内存中有两份几乎同样的接近4GB 的 bytes 对象,一个叫 r 一个叫 r2
r2[:2] = b"xx"
那么有没有更好的解决方案可以节省调上面拷贝的内存开销呢? 我想要好几个对象都共享这同一个 bytearray 并且分别进行修改怎么办呢?
python 引入了一个 buffer protocol 来解决这个问题, 详见: PEP 3118 -- Revising the buffer protocol
# r 是一个 bytearray, 初始像操作系统分配了 4GB 的空间
r = bytearray(2 ** 32)
# socket 读到的内容直接往 r 里面写,而不是存储到一个 bytes 对象里
connection.readinto(r)
那么我们来看下 buffer protocol 在 PyByteArrayObject 中的实现
/* 支持 buffer protocol 的对象会定义
getbufferproc 和 releasebufferproc 两个特征的函数作为接口
调用者需要获取某对象的共享空间时调用 getbufferproc,释放时调用
*/
/* Objects/bytearrayobject.c
2144 - 2147 行
定义了接口
*/
static PyBufferProcs bytearray_as_buffer = {
(getbufferproc)bytearray_getbuffer,
(releasebufferproc)bytearray_releasebuffer,
};
/* Objects/bytearrayobject.c
63 - 77 行
*/
static int
bytearray_getbuffer(PyByteArrayObject *obj, Py_buffer *view, int flags)
{
/* 外部调用者需要把 view 指向 obj 的时候调用 */
void *ptr;
if (view == NULL) {
PyErr_SetString(PyExc_BufferError,
"bytearray_getbuffer: view==NULL argument is obsolete");
return -1;
}
/* 把 obj 的信息填充到 view 中,使得调用者使用 view 的时候和 使用 obj 时一致 */
ptr = (void *) PyByteArray_AS_STRING(obj);
/* cannot fail if view != NULL and readonly == 0 */
(void)PyBuffer_FillInfo(view, (PyObject*)obj, ptr, Py_SIZE(obj), 0, flags);
/* 当有调用者需要获取共享空间时,ob_exports 增加1 表示当前被共享的数量 */
obj->ob_exports++;
return 0;
}
/* Objects/bytearrayobject.c
65 - 69 行
*/
static void
bytearray_releasebuffer(PyByteArrayObject *obj, Py_buffer *view)
{
/* 当有调用者释放改共享空间时,ob_exports 减小1 */
obj->ob_exports--;
}
memoryview
buffer protocol 是给写 c 程序扩展的开发者使用的,一般我们写 python 程序的时候,可以借助 memoryview 来使用 buffer protocol
# r 是一个 bytearray, 初始像操作系统分配了 4GB 的空间
r = bytearray(2 ** 32)
# socket 读到的内容直接往 r 里面写,而不是存储到一个 bytes 对象里
connection.readinto(r)
# 此时我需要再创建一个 r2, 但是我想和 r 共享同一个空间
r2 = memoryview(r)
func(r2)
参考资料:
Does Python bytearray use signed integers in the C representation?
Less copies in Python with the buffer protocol and memoryviews