python源码剖析No.6--bytes对象

不少编程语言中的字符串都是有字符数组(又称字节序列)来表示,比如C:

1
char msg[] = "Hello World!";

由于1 Byte8bit空间,具有265种排列方式,因此单个字节的表现范围有限,对于英文字符以及常用的符号字符可能够,但为了兼容其他字符(伟大的汉字),计算机的先驱们发明了多字节编码 – 通过多个字节来表示一个字符。

ps:这让我联想起python中实现大整数的方式了–串联多个C整型来表达。

Python提供的解决方案是Unicode字符串(str)对象,unicode 可以表示各种字符,无需关心编码。

然而,存储或者网络通讯时,字符串对象无法避免的需要序列化成字节序列进行传输。为此,python额外提供了字节序列对象--bytes

简单理解就是和用户做交互的是str对象,是所容易理解的。而和机器做交互则是越简单越好,因此是序列化后的字位码

本节所探讨的是python中的bytes对象

bytes对象定义

python中的bytes对象定义在/Objects/bytesobject.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef Py_LIMITED_API
typedef struct {
PyObject_VAR_HEAD
Py_hash_t ob_shash;
char ob_sval[1];

/* Invariants:
* ob_sval contains space for 'ob_size+1' elements.
* ob_sval[ob_size] == 0.
* ob_shash is the hash of the string or -1 if not computed yet.
*/
} PyBytesObject;
#endif

Py_LIMITED_API查到是和python版本挂钩的东西,这里不管。

可以看到PyBytesObject是变长对象,除了变长对象的共有头部PyObject_VAR_HEAD外,还提供了存放对象hash值结果的ob_shash字段,再就是存放bytes对象内容的ob_sval字符数组了。

从上述定义中相关的描述,我们还可以知道:

  • ob_sval包含了PyObject_VAR_HEAD.ob_size长度+1个元素,且最后一个内容存放0,标志结束

  • ob_shash使用来存放对象hash计算值的,当为-1时,说明尚未进行对象hash计算。之所以设置这个字段是由于python对象哈希值的应用范围很大,比如dict字典对象以来对象哈希值进行存储,由于便利计算bytes对象哈希值需要遍历其内部的字符数组,开销相对大,因此python选择将哈希值保存起来,以空间换时间,避免重复计算。(smart)

1
2
3
4
import sys
sys.getsizeof(b'')
#--------output-------#
33

从对象结构来回答这问题:

python在分配bytes对象空间对于ob_sval的元素会多分配一个,用于存放b'00',头部及ob_shash的每个字段占据8字节(64位系统下),因此大小为33

创建也是类似之前探讨过的对象:

bytes对象行为

之前我们学习到探讨对象行为,需要从对象的类型出发,这里如出一辙,找到PyBytes_Type,定位到代码位置:/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
39
40
41
42
PyTypeObject PyBytes_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"bytes",
PyBytesObject_SIZE,
sizeof(char),
bytes_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
(reprfunc)bytes_repr, /* tp_repr */
&bytes_as_number, /* tp_as_number */
&bytes_as_sequence, /* tp_as_sequence */
&bytes_as_mapping, /* tp_as_mapping */
(hashfunc)bytes_hash, /* tp_hash */
0, /* tp_call */
bytes_str, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
&bytes_as_buffer, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_BYTES_SUBCLASS, /* tp_flags */
bytes_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
(richcmpfunc)bytes_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
bytes_iter, /* tp_iter */
0, /* tp_iternext */
bytes_methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
&PyBaseObject_Type, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
bytes_new, /* tp_new */
PyObject_Del, /* tp_free */
};

从上面的PyBytes_Type构成,可以看出PyBytesObject支持的操作类型有数值型操作、序列型操作、关系型操作。

数值型操作

跟踪bytes_as_number字段,可以看到bytes对象支持的数值型操作

1
2
3
4
5
6
static PyNumberMethods bytes_as_number = {
0, /*nb_add*/
0, /*nb_subtract*/
0, /*nb_multiply*/
bytes_mod, /*nb_remainder*/
};

bytes对象竟然可以进行数值型操作?!跟进查看:bytes_mod

1
2
3
4
5
6
7
8
9
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);
}

关键是_PyBytes_FormatEx,从名字上看,是做了类C的格式化输出。

序列型操作

跟踪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_length,跟进发现定义位置:

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

可以看到,是通过返回PyVarObject.ob_size来获取bytes对象的长度的.

再比如bytes_concat初看API名称,应该猜得出大概是bytes对象追加内容的功能,顺藤摸瓜

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
/* This is also used by PyBytes_Concat() */
static PyObject *
bytes_concat(PyObject *a, PyObject *b)
{
Py_buffer 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)) {
result = b;
Py_INCREF(result);
goto done;
}
if (vb.len == 0 && PyBytes_CheckExact(a)) {
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);
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;
}
  • 第15行,之前完成的事两个临时对象的申请,规模同传递进来的两个操作对象,并填充进临时对象
  • 第18-22行,当第一个操作对象va的长度为0,则函数执行结果为对象vb,进行返回
  • 第23-27行,类似上面的过程。
  • 第29-32行,判断va是否有充足的空间进行追加,不够则返回错误
  • 第34-38行,重新申请足够大的bytes对象,逐一完成对象va vb内容的拷贝

以上就是bytes_concat实现的细节了。至于bytes对象的其他序列型操作,读者可自行分析。

关联型操作

跟踪bytes_as_mapping字段,来到

1
2
3
4
5
static PyMappingMethods bytes_as_mapping = {
(lenfunc)bytes_length,
(binaryfunc)bytes_subscript,
0,
};

可以看到,支持的操作,第一个先前分析过的bytes_length还支持bytes_subscript

数据拷贝的陷阱

考虑一下三个PyBytesObject的合并(concat)

1
result = a + b + c

实际上在python内部运行如下:

1
2
tmp = a + b
result = tmp + c

类比的发现,随着待合并的内容越多,则开销会增大。(只有最后一个对象不会重复拷贝,其余对象会拷贝n-1次)

使用join方法实现

join方法对数据拷贝进行了优化:现遍历待合并对象,得到所有对象的ob_size,计算总长度。根据这个值创建长度合适的result对象存放结果,再遍历待合并对象,逐一拷贝数据。

字符缓冲池

为了优化单字节bytes对象的创建效率,python内部维护了一个字符缓冲池,不同于小整数对象缓冲池的是字符缓冲池一开始为空的,随着运行逐渐添加,相关定义

1
static PyBytesObject *characters[UCHAR_MAX + 1];

python内部创建单字节bytes对象时,先检查目标对象是否再缓冲池中。

根据注释内容,找得到相应的bytes对象创建方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
For PyBytes_FromString(), the parameter `str' points to a null-terminated
string containing exactly `size' bytes.

For PyBytes_FromStringAndSize(), the parameter `str' is
either NULL or else points to a string containing at least `size' bytes.
For PyBytes_FromStringAndSize(), the string in the `str' parameter does
not have to be null-terminated. (Therefore it is safe to construct a
substring by calling `PyBytes_FromStringAndSize(origstring, substrlen)'.)
If `str' is NULL then PyBytes_FromStringAndSize() will allocate `size+1'
bytes (setting the last byte to the null terminating character) and you can
fill in the data yourself. If `str' is non-NULL then the resulting
PyBytes object must be treated as immutable and you must not fill in nor
alter the data yourself, since the strings may be shared.

The PyObject member `op->ob_size', which denotes the number of "extra
items" in a variable-size object, will contain the number of bytes
allocated for string data, not counting the null terminating character.
It is therefore equal to the `size' parameter (for
PyBytes_FromStringAndSize()) or the length of the string in the `str'
parameter (for PyBytes_FromString()).
*/

python创建bytes对象使用的是PyBytes_FromString`PyBytes_FromStringAndSize`

先看第一个PyBytes_FromString

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
PyObject *
PyBytes_FromString(const char *str)
{
size_t size;
PyBytesObject *op;

assert(str != NULL);
size = strlen(str);
if (size > PY_SSIZE_T_MAX - PyBytesObject_SIZE) {
PyErr_SetString(PyExc_OverflowError,
"byte string is too long");
return NULL;
}
if (size == 0 && (op = nullstring) != NULL) {
#ifdef COUNT_ALLOCS
null_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}

/* Inline PyObject_NewVar */
op = (PyBytesObject *)PyObject_MALLOC(PyBytesObject_SIZE + size);
if (op == NULL)
return PyErr_NoMemory();
(void)PyObject_INIT_VAR(op, &PyBytes_Type, size);
op->ob_shash = -1; //PyBytesObject.ob_shash未计算
memcpy(op->ob_sval, str, size+1);
/* share short strings */
if (size == 0) {
nullstring = op;
Py_INCREF(op);
} else if (size == 1) {
characters[*str & UCHAR_MAX] = op;
//执行到这里,说明单字节缓冲池中没有当前申请的单字节对象,因此放入单字节缓冲池中
Py_INCREF(op);
}
return (PyObject *) op;
}

可以看到第21-27行,就是当创建的bytes对象是单字节,且在单字节缓冲池中,则直接添加缓冲池中目标的引用计数,随后返回。one_strings是用于计数当前单字节缓冲池的使用情况

在看第二个PyBytes_FromStringAndSize

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
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;
}

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;
}

第10-18行,对于单字节对象的创建类似。其中关键的语句是

if (size == 1 && str != NULL &&(op = characters[*str & UCHAR_MAX]) != NULL)解释:当创建的是单字节对象 && 内容是不为NULL && 内容不在单字节缓冲池中。

疑问

但貌似没看到放入缓冲池的操作,先留个疑问吧。从代码上看貌似只是将缓冲池使用计数的one_strings++而已,后续在此分配怎么直接引用的?hash?

有没有老哥对于这点有思路的?

好吧,自己画了遍下边的示意图:

发现貌似自己能解答这个问题了(个人理解,可能存在错误),也不修改了,当作个纪念,告诉自己,勤动手才是正确的学习方法。

我们重新贴一遍单bytes对象创建的关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (size == 1 && str != NULL && (op = characters[*str & UCHAR_MAX]) != NULL)
{
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}

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);
}

one_strings是用于计数当前单字节缓冲池中的使用情况。python初始该值为0,随着运行的进行,每当单字节缓冲池中的内容被使用,则该计数+1,以上图为例,假设标记出的几个被使用了,且其他为NULL,则one_strings = 1为什么?,先留着疑问,我们往下看:

op = characters[*str & UCHAR_MAX]) != NULL

由于单字节缓冲池中的数量是0-255(映射为ascii码)当创建的是单字节,就尝试进行与操作characters[*str & UCHAR_MAX],取出相应位置

  • 当为空,则说明缓冲池中该对象为NULL,则申请相应对象,并修改缓冲池中相应指向该对象

  • 不为空,则说明缓冲池中有该对象则取出使用,one_strings++


精彩面试题

1
2
3
4
5
6
7
8
9
>>> a1 = b'a'
>>> a2 = b'a'
>>> a1 is a2
True

>>> ab1 = b'ab'
>>> ab2 = b'ab'
>>> ab1 is ab2
False

导致上述现象存在的原因是由于python单字节缓冲池存在的原因

Author: Victory+
Link: https://cvjark.github.io/2022/05/23/python源码剖析-bytes对象/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.