楔子
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 的缓冲区了。