长亭百川云 - 文章详情

通过 bytes 对象的合并,探究缓冲区的奥秘

古明地觉的编程教室

40

2024-07-13

楔子

bytes 对象支持加法运算,将两个 bytes 对象合并为一个,举个例子。

b1 = b"abc"  
b2 = b"def"  
print(b1 + b2)  # b'abcdef'

这背后是怎么实现的呢?我们通过源码分析一下,并通过 bytes 对象的相加,介绍一下缓冲区的知识。

bytes 对象的加法运算

提到加法,很容易联想到 PyNumberMethods 的 nb_add,比如:PyLongObject 的 long_add 和 PyFloatObject 的 float_add。

但对于 bytes 对象而言却不是这样,加法操作对应的是 PySequenceMethods 的 sq_concat。所以我们将加法运算改成合并,会更合适一些,只是它在 Python 层面对应的也是 + 操作符。

对于 bytes 对象而言,sq_concat 字段会被赋值为 bytes_concat。

// Objects/bytesobject.c  
static PyObject *  
bytes_concat(PyObject *a, PyObject *b)  
{     
    // 两个 Py_buffer 结构体类型的变量,用于维护缓冲区  
    // 关于缓冲区,我们一会儿说  
    Py_buffer va, vb;  
    // 相加结果  
    PyObject *result = NULL;  
    // 此时缓冲区啥也没有,默认将缓冲区的长度初始化为 -1  
    va.len = -1;  
    vb.len = -1;  
    // 每个 bytes 对象底层都对应一个缓冲区,可以通过 PyObject_GetBuffer 获取  
    // 获取两个 bytes 对象的缓冲区,交给变量 va 和 vb  
    // 获取成功返回 0,获取失败返回非 0  
    // 如果下面的条件不成功,就意味着获取失败了,说明至少有一个老铁不是 bytes 类型  
    if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 ||  
        PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0) {  
        // 然后设置异常,PyExc_TypeError 表示 TypeError(类型错误)  
        // 专门用来表示对一个对象执行了它所不支持的操作  
        PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s",  
                     Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name);  
        // 比如 b"123" + 123 就会得到 TypeError: can't concat int to bytes  
        // 和这里设置的异常信息是一样的,然后当出现异常之后,直接跳转到 done 标签  
        goto done;  
    }  
  
    // 这里判断是否有一方长度为 0  
    // 如果 a 的长度为 0,那么相加之后结果就是 b  
    if (va.len == 0 && PyBytes_CheckExact(b)) {  
        // Py_NewRef(obj) 会增加 obj 指向对象的引用计数,同时返回 obj  
        // 所以增加引用计数之后将 b 赋值给 result,两者指向同一个对象  
        result = Py_NewRef(b);  
        goto done;  
    }  
    // 逻辑和上面类似,如果 b 长度为 0,那么相加之后的结果就是 a  
    if (vb.len == 0 && PyBytes_CheckExact(a)) {  
        result = Py_NewRef(a);  
        goto done;  
    }  
    // 判断两个 bytes 对象合并之后,长度是否超过 PY_SSIZE_T_MAX  
    // 所以 bytes 对象是有长度限制的,因为维护长度的 ob_size 有最大范围  
    // 但还是之前说的,这个条件基本不可能满足,除非你写恶意代码  
    // 补充一句,这个 if 条件看起来会有些别扭,更直观的写法应该像下面这样  
    // if (va.len + vb.len > PY_SSIZE_T_MAX),但 va.len + vb.len 可能会溢出  
    if (va.len > PY_SSIZE_T_MAX - vb.len) {  
        PyErr_NoMemory();  
        goto done;  
    }  
    // 否则的话,创建指定容量的 PyBytesObject  
    result = PyBytes_FromStringAndSize(NULL, va.len + vb.len);  
    if (result != NULL) {  
        // PyBytes_AS_STRING 会获取 PyBytesObject 的 ob_sval 字段  
        // 将缓冲区 va 里面的内容拷贝到 result->ob_sval 中,拷贝的长度为 va.len  
        memcpy(PyBytes_AS_STRING(result), va.buf, va.len);  
        // 将缓冲区 vb 里面的内容拷贝到 result->ob_sval 中,拷贝的长度为 vb.len  
        // 但是从 va.len 的位置开始拷贝,不然会把之前的内容覆盖掉  
        memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);  
    }  
  
  done:  
    // 拷贝完之后,将 va 和 vb 里的内容释放掉,否则可能会导致内存泄漏  
    if (va.len != -1)  
        PyBuffer_Release(&va);  
    if (vb.len != -1)  
        PyBuffer_Release(&vb);  
    return result;  
}

代码虽然有点长,但是不难理解,重点是里面的 Py_buffer。我们以 a = b"ab",b = b"cde" 为例,看一下 a + b 是怎么做的?

说白了整个过程就是将 a->ob_sval 和 b->ob_sval 拷贝到 result->ob_sval 中。但问题是为啥不直接拷贝,而是要搞出来一个 Py_buffer 呢?这就要说一说 Python 的缓冲区了。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2