由于一个字节8bit最多只能表示 256 种字符,用来表示英文字符绰绰有余,想覆盖非英文字符便捉襟见肘了。为了表示众多的非英文字符(比如汉字),计算机先驱们发明了 多字节编码 ——通过 多个字节来表示一个字符。由于 原始字节序列不维护编码信息,操作不慎便导致各种乱码现象。
Python 提供的解决方案是 Unicode 字符串 ( str )对象, Unicode 可以表示各种字符,无需关心编码。然而存储或者网络通讯时,字符串对象不可避免要 序列化 成字节序列。为此, Python 额外提供了 字节序列对象—— bytes 。

如上图, str 对象统一表示一个 字符串 ,不需要关心编码;计算机通过 字节序列 与存储介质和网络介质打交道,字节序列由 bytes 对象表示;存储或传输 str 对象时,需要将其 序列化 成字节序列,序列化过程也是 编码 的过程。
对象结构#
bytes 对象用于表示由若干字节组成的 字节序列 以及相关的 操作 ,并不关心字节序列的 含义 。因此, bytes 应该一种 变长 、 不可变 对象 ,内部由 C 数组 实现。如下图:

ob_sval 字节序列对象 PyBytesObject 中,确实藏着一个字符数组 ob_sval 。注意到 ob_sval 数组长度定义为 1 ,这是 C 语言中定义 变长数组 的技巧(ob_sval存储的是地址)。
ob_snash ob_shash ,它用于保存字节序列的 哈希值 。 由于计算 bytes 对象哈希值需要遍历其内部的字符数组,开销相对较大。因此, Python 第一次计算 哈希值时,选择 将哈希值缓存到 ob_shash字段中,以 空间换时间,避免重复计算。
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
12PyTypeObject 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 */
// ...
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16static 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);
}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
10static 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*/
};
长度#
最简单的序列型操作是 长度查询 ,直接返回 ob_size 字段即可:
1
2
3
4
5static Py_ssize_t
bytes_length(PyBytesObject *a)
{
return Py_SIZE(a);
}
合并#
1 | >>> b'abc' + b'cba' |
合并操作将两个 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
46static 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; //返回结果
}
数据拷贝的陷阱#
考察以下表达式——合并 3 个 bytes 对象:
1
>>> result = a + b + c
1
2>>> t = a + b
>>> result = t + c
合并 n 个 bytes 对象,头两个对象需要拷贝 n-1 次,只有最后一个对象不需要重复拷贝。平均下来,每个对象大约要拷贝 n/2 次!
内建方法 join#
bytes 对象提供了一个内建方法 join ,可高效合并多个 bytes 对象:1 | >>> result = b''.join(segments) |
字符缓冲池#
为了优化单字节 bytes 对象(也可称为 字符对象
)的创建效率, Python 内部维护了一个 字符缓冲池 :
1
static PyBytesObject *characters[UCHAR_MAX + 1];
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
38PyObject *
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 个,但使用频率非常高。缓冲池技术作为一种 以空间换时间 的优化手段,只需 较小的内存为代价,便可明显提升执行效率。