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

cpython 源码分析 PyByteArrayObject(bytearray)

 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