0%

[python源码分析] 5.bytes

由于一个字节8bit最多只能表示 256 种字符,用来表示英文字符绰绰有余,想覆盖非英文字符便捉襟见肘了。为了表示众多的非英文字符(比如汉字),计算机先驱们发明了 多字节编码 ——通过 多个字节来表示一个字符。由于 原始字节序列不维护编码信息,操作不慎便导致各种乱码现象。

Python 提供的解决方案是 Unicode 字符串 ( str )对象, Unicode 可以表示各种字符,无需关心编码。然而存储或者网络通讯时,字符串对象不可避免要 序列化 成字节序列。为此, Python 额外提供了 字节序列对象—— bytes

如上图, str 对象统一表示一个 字符串 ,不需要关心编码;计算机通过 字节序列 与存储介质和网络介质打交道,字节序列由 bytes 对象表示;存储或传输 str 对象时,需要将其 序列化 成字节序列,序列化过程也是 编码 的过程。

对象结构#

bytes 对象用于表示由若干字节组成的 字节序列 以及相关的 操作 ,并不关心字节序列的 含义 。因此, bytes 应该一种 变长 、 不可变 对象 ,内部由 C 数组 实现。如下图:

  1. ob_sval 字节序列对象 PyBytesObject 中,确实藏着一个字符数组 ob_sval 。注意到 ob_sval 数组长度定义为 1 ,这是 C 语言中定义 变长数组 的技巧(ob_sval存储的是地址)。

  2. ob_snash ob_shash ,它用于保存字节序列的 哈希值 。 由于计算 bytes 对象哈希值需要遍历其内部的字符数组,开销相对较大。因此, Python 第一次计算 哈希值时,选择 将哈希值缓存到 ob_shash字段中,以 空间换时间,避免重复计算。

  3. ob_size 每个PyVarObject内部都有个 ob_size字段,PyBytesObject使用此字段存储大小信息以 保持len()操作的O(1)时间复杂度,并跟踪非ascii字符串的大小(内部可以为空字符)

空对象样例#

Python 为待存储的字节序列 额外分配一个字节,用于在末尾处保存 \0 ,以便兼容 C 字符串。从上图可以看出,就算空 bytes 对象( b'' )也是要占用内存空间的,至少变长对象 公共头部 是少不了的。

1
2
>>> sys.getsizeof(b'')
33

bytes 对象占用的内存空间可分为以下个部分进行计算:

  • PyVarObject公共头部 24 字节,ob_refcnt 、 ob_type 、 ob_size 每个字段各占用 8 字节;
  • 哈希值 ob_shash 占用 8 字节;
  • 字节序列本身,假设是 n 字节;
  • 额外 1 字节用于存储末尾处的 \0 ;

因此,bytes 对象空间计算公式为 24+8+n+124+8+n+1,即 33+n33+n,其中 n 为字节序列长度(也是len的取值)。 经过上面的学习,我们可以知道 len(byte对象) = n,len显示的只是 ob_size字段的值,而bytes对象真实占用内存量还 需要加 33.

ascii样例#

对象行为#

对象的行为由对象的 类型 决定,因而我们需要到 bytes 类型对象(PyBytes_Type)中寻找答案。在 Objects/bytesobject.c 源码文件中,我们找到 bytes 类型对象 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
PyTypeObject PyBytes_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"bytes",
PyBytesObject_SIZE,
sizeof(char),
// ...
&bytes_as_number, /* tp_as_number 保存着 数值运算 处理函数的指针*/
&bytes_as_sequence, /* tp_as_sequence */
&bytes_as_mapping, /* tp_as_mapping */
(hashfunc)bytes_hash, /* tp_hash */
// ...
};
bytes 对象居然支持数据操作?bytes_as_number 结构体中只定义了一个操作—— 模运算 ( % ):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static PyNumberMethods bytes_as_number = {
0, /*nb_add*/
0, /*nb_subtract*/
0, /*nb_multiply*/
bytes_mod, /*nb_remainder*/
}
static PyObject *
bytes_mod(PyObject *self, PyObject *arg)
{
if (!PyBytes_Check(self)) {
Py_RETURN_NOTIMPLEMENTED;
}
//实现字符串格式化
return _PyBytes_FormatEx(PyBytes_AS_STRING(self), PyBytes_GET_SIZE(self),
arg, 0);
}
由此可见, bytes 对象只是 借用 % 运算符实现字符串格式化,谈不上支持数值运算,虚惊一场:
1
2
>>> b'msg: a=%d b=%d' % (1, 2)
b'msg: a=1 b=2'

序列型操作#

众所周知, bytes 是 序列型对象 ,序列型操作才是研究重点。我们在 bytes_as_sequence 结构体中找到相关定义:

1
2
3
4
5
6
7
8
9
10
static PySequenceMethods bytes_as_sequence = {
(lenfunc)bytes_length, /*sq_length*/
(binaryfunc)bytes_concat, /*sq_concat*/
(ssizeargfunc)bytes_repeat, /*sq_repeat*/
(ssizeargfunc)bytes_item, /*sq_item*/
0, /*sq_slice*/
0, /*sq_ass_item*/
0, /*sq_ass_slice*/
(objobjproc)bytes_contains /*sq_contains*/
};
由此可见, bytes 支持的 序列型操作 包括以下 5 个: - sq_length ,查询序列长度; - sq_concat ,将两个序列合并为一个; - sq_repeat ,将序列重复多次; - sq_item ,取出给定下标序列元素; - sq_contains,包含关系判断;

长度#

最简单的序列型操作是 长度查询 ,直接返回 ob_size 字段即可:

1
2
3
4
5
static Py_ssize_t
bytes_length(PyBytesObject *a)
{
return Py_SIZE(a);
}

合并#

1
2
>>> b'abc' + b'cba'
b'abccba'

合并操作将两个 bytes 对象拼接成一个,由 bytes_concat 函数处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static PyObject *
bytes_concat(PyObject *a, PyObject *b)
{
Py_buffer va, vb; //定义局部变量 va 、 vb 用于维护缓冲区
PyObject *result = NULL; //新建临时变量,保存合并结果

va.len = -1;
vb.len = -1;
//获取字节序列所在缓冲区
if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 ||
PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0) {
PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s",
Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name);
goto done;
}

/* Optimize end cases */
if (va.len == 0 && PyBytes_CheckExact(b)) { //如果第一个对象长度为 0 ,第二个对象就是结果
result = b;
Py_INCREF(result);
goto done;
}
if (vb.len == 0 && PyBytes_CheckExact(a)) { //第二个对象长度为 0 ,第一个对象就是结果
result = a;
Py_INCREF(result);
goto done;
}

if (va.len > PY_SSIZE_T_MAX - vb.len) { //长度超过限制则报错
PyErr_NoMemory();
goto done;
}

result = PyBytes_FromStringAndSize(NULL, va.len + vb.len); //临时 bytes 对象用于保存合并结果,长度为待合并对象长度之和
if (result != NULL) {
memcpy(PyBytes_AS_STRING(result), va.buf, va.len);
memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);
}

done:
if (va.len != -1)
PyBuffer_Release(&va);
if (vb.len != -1)
PyBuffer_Release(&vb);
return result; //返回结果
}
bytes_concat 函数逻辑很直白,将两个 bytes 对象的缓冲区拷贝到一起形成新 bytes 对象。

数据拷贝的陷阱#

考察以下表达式——合并 3 个 bytes 对象:

1
>>> result = a + b + c
这个语句执行时,分成两步进行合并:先将 a 和 b 合并,得到临时结果 t ,再将 t 和 c 合并得到最终结果 result :
1
2
>>> t = a + b
>>> result = t + c
这个过程中,a 和 b 的数据需要被拷贝两遍

合并 n 个 bytes 对象,头两个对象需要拷贝 n-1 次,只有最后一个对象不需要重复拷贝。平均下来,每个对象大约要拷贝 n/2 次!

内建方法 join#

bytes 对象提供了一个内建方法 join ,可高效合并多个 bytes 对象:
1
>>> result = b''.join(segments)
join 方法对数据拷贝进行了优化:先遍历待合并对象计算总长度;然后根据总长度 创建目标对象;最后再 遍历待合并对象,逐一拷贝数据。这样一来,每个对象均只需拷贝一次,解决了重复拷贝的陷阱。

字符缓冲池#

为了优化单字节 bytes 对象(也可称为 字符对象 )的创建效率, Python 内部维护了一个 字符缓冲池 :

1
static PyBytesObject *characters[UCHAR_MAX + 1];
Python 内部 创建单字节 bytes 对象时,先检查目标对象是否已在缓冲池中。PyBytes_FromStringAndSize 函数是负责创建 bytes 对象的通用接口,同样位于 Objects/bytesobject.c 中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
PyObject *
PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)
{
PyBytesObject *op;
if (size < 0) {
PyErr_SetString(PyExc_SystemError,
"Negative size passed to PyBytes_FromStringAndSize");
return NULL;
}

//如果目标对象为 单字节对象 且 已在字符缓冲池 中,直接返回已缓存对象
if (size == 1 && str != NULL &&
(op = characters[*str & UCHAR_MAX]) != NULL)
{
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}

//创建新 bytes 对象并拷贝字节序列
op = (PyBytesObject *)_PyBytes_FromSize(size, 0);
if (op == NULL)
return NULL;
if (str == NULL)
return (PyObject *) op;

memcpy(op->ob_sval, str, size);
/* share short strings */

//如果创建的对象为单字节对象,将其放入字符缓冲池
if (size == 1) {
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
return (PyObject *) op;
}

由此可见,当 Python 程序 开始运行时字符缓冲池是空的。随着 单字节 bytes 对象的创建,缓冲池中的对象慢慢多了起来。

字符对象 首次创建后便在缓冲池中缓存起来;后续再次使用时, Python 直接从缓冲池中取,避免重复创建和销毁。与 小整数 一样,字符对象 只有为数不多的 256 个,但使用频率非常高。缓冲池技术作为一种 以空间换时间 的优化手段,只需 较小的内存为代价,便可明显提升执行效率。

reference#

  1. Python 源码深度剖析/09 bytes 对象,不可变的字节序列
  2. bytes