06-Bytes对象的底层操作 楔子 不少编程语言中的”字符串”都是使用字符数组(或者称字符序列)来表示,比如C语言和go语言就是这样。
1 char name[] = "komeiji satori" ;
一个字节最多能表示256个字符,所以对于英文来说足够了,因此一个英文字符占一个字节即可,然而对于那些非英文字符便力不从心了。因此为了表示这些非英文编码,于是多字节编码应运而生—-通过多个字节来表示一个字符。但由于原始字节序列不维护编码信息,因此操作不慎便导致各种乱码现象。
而Python提供的解决方案是使用unicode(在Python3中等价于str)
表示字符串,因为unicode可以表示各种字符,不需要关心编码的问题。但在存储或网络通讯时,字符串不可避免地要序列化成字节序列。为此,Python除了提供字符串对象之外,还额外提供了字节序列对象—-bytes。
如上图,str对象统一表示一个字符串,不需要关心编码;计算机通过字节序列和存储介质、网络介质打交道,字节序列由bytes对象表示;在存储和传输str对象的时候,需要将其序列化成字节序列,序列化也是编码的过程。
下面我们就来看看bytes对象在底层的数据结构。
PyBytesObject 我们说bytes对象是由若干个字节组成的,显然这是一个变长对象,有多少个字节说明其长度是多少。
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct { PyObject_VAR_HEAD Py_hash_t ob_shash; char ob_sval[1 ]; } PyBytesObject;
我们看一下里面的成员对象:
PyObject_VAR_HEAD:变长对象的公共头部
ob_shash:保存该字节序列的哈希值,之所以选择保存是因为在很多场景都需要bytes对象的哈希值。而Python在计算字节序列的哈希值的时候,需要遍历每一个字节,因此开销比较大。所以会提前计算一次并保存起来,这样以后就不需要算了,可以直接拿来用,并且bytes对象是不可变的,所以哈希值是不变的。
ob_sval:这个和PyLongObject中的ob_digit的声明方式是类似的,虽然声明的时候长度是1, 但具体是多少则取决于bytes对象的字节数量。这是C语言中定义"变长数组"的技巧, 虽然写的长度是1, 但是你可以当成n来用, n可取任意值。显然这个ob_sval存储的是所有的字节,因此Python中的bytes的值,底层是通过字符数组存储的。而且通过注释,我们发现会多申请一个空间,用于存储\0,因为C中是通过\0来表示一个字符数组的结束,但是计算ob_size的时候不包括\0。
我们创建几个不同的bytes对象,然后通过画图感受一下:
val = b””
我们看到一个空的字节序列,底层的ob_savl也是需要一个’\0’的,那么这个结构体实例占多大内存呢?我们说上面ob_sval之外的四个成员,显然每个都是8字节,而ob_savl每个成员都是一个char、也就是占1字节,所以Python中bytes对象占的内存等于32 + ob_sval的长度。而ob_sval里面至少有一个’\0’,因此对于一个空的字节序列,显然占33个字节。注意:ob_size统计的是ob_sval中有效字节的个数,不包括’\0’,但是计算占用内存的时候,显然是需要考虑在内的,因为它确实多占用了一个字节的空间。或者说bytes对象占的内存等于33 + ob_size也是可以的。
1 2 3 4 >>> val = b"" >>> sys.getsizeof(val)33 >>>
val = b”abc”
1 2 3 4 >>> val = b"abc" >>> sys.getsizeof(val)36 >>>
bytes对象的行为 介绍bytes对象在底层的数据结构之后,我们要考察bytes对象的行为。我们说实例对象的行为由其类型对象决定,所以bytes对象具有哪些行为,就看bytes类型对象本身定义了哪些操作。bytes类型对象,显然对应PyBytes_Type,根据我们之前介绍的规律,也可以猜出来,它定义在Object/bytesobject.c中。
1 2 3 4 5 6 7 8 9 10 11 12 13 PyTypeObject PyBytes_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0 ) "bytes" , PyBytesObject_SIZE, sizeof (char ), &bytes_as_number, &bytes_as_sequence, &bytes_as_mapping, (hashfunc)bytes_hash, };
到了现在,相信你对类型对象的结构肯定非常熟悉了,因为类型对象都是由PyTypeObject结构体实例化得到的。我们看到tp_as_number,它居然不是0,而是传递了一个指针,说明确实指向了一个PyNumberMethods结构体实例。难道bytes支持数值运算,这显然是不可能的啊,所以我们需要进入bytes_as_number中一探究竟。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static PyNumberMethods bytes_as_number = { 0 , 0 , 0 , bytes_mod, } 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对象只是借用了%运算实现了格式化,谈不上数值运算,虚惊一场。不过由此也看到了Python的动态特性,即使是相同的操作,但如果是不同类型的对象执行的话,也会有不同的表现。
1 2 3 4 >>> info = b"name: %s, age: %d" >>> info % (b"satori" , 16 )b'name: satori, age: 16' >>>
除了tp_as_number,PyBytes_Type还给tp_as_sequence成员传递了bytes_as_sequence指针,说明bytes对象支持序列操作。显然这是肯定的,而且bytes对象显然是序列型对象,所以序列型操作才是我们的研究的重点,下面看看bytes_as_sequence的定义。
1 2 3 4 5 6 7 8 9 10 static PySequenceMethods bytes_as_sequence = { (lenfunc)bytes_length, (binaryfunc)bytes_concat, (ssizeargfunc)bytes_repeat, (ssizeargfunc)bytes_item, 0 , 0 , 0 , (objobjproc)bytes_contains };
根据定义我们看到,bytes对象支持的序列型操作一共有5个:
sq_length:查看序列的长度
sq_concat:将两个序列合并为一个
sq_repeat:将序列重复多次
sq_item:根据索引获取指定的下表, 得到一个整型;如果是切片,那么还会得到一个bytes对象
sq_contains:判断某个序列是不是在该序列中,显然它等价于Python中的in操作
查看序列长度:
显然这是最简单的,直接获取ob_size即可,比如:val = b”abcde”,那么长度就是5。
1 2 3 4 5 static Py_ssize_tbytes_length (PyBytesObject *a) { return Py_SIZE(a); }
将两个序列合并为一个:
1 2 3 4 5 >>> a = b"abc" >>> b = b"def" >>> a + bb'abcdef' >>>
而且我们看到这里相当于是加法运算,我们很容易想到会是PyNumberMethods中的nb_add,比如:PyLongObject对应的long_add、PyFloatObject对应的float_add,但对于bytes对象而言,加法操作对应PySequenceMethods的sq_concat。所以我们看到Python中的同一个操作符,在底层会对应不同的函数,比如:long_add和float_add、以及这里的bytes_concat,在Python的层面都是+这个操作符。然后我们看看底层是怎么对两个字节序列进行相加的。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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; } 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; }
虽然代码很长,但是不难理解。不过可能有人认为为什么非要先将a、b的内容拷贝到Py_buffer里面,再通过Py_buffer拷贝到result里面去呢?直接拷贝不可以吗?答案是Py_buffer提供了一套操作对象缓冲区的统一接口,屏蔽不同类型对象的内部差异。
将序列重复多次:
1 2 3 4 5 6 >>> a = b"abc" >>> a * 3 b'abcabcabc' >>> a * -1 b'' >>>
然后我们看看底层的实现:
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 static PyObject *bytes_repeat (PyBytesObject *a, Py_ssize_t n) { Py_ssize_t i; Py_ssize_t j; Py_ssize_t size; PyBytesObject *op; size_t nbytes; if (n < 0 ) n = 0 ; if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n) { PyErr_SetString(PyExc_OverflowError, "repeated bytes are too long" ); return NULL ; } size = Py_SIZE(a) * n; if (size == Py_SIZE(a) && PyBytes_CheckExact(a)) { Py_INCREF(a); return (PyObject *)a; } nbytes = (size_t )size; if (nbytes + PyBytesObject_SIZE <= nbytes) { PyErr_SetString(PyExc_OverflowError, "repeated bytes are too long" ); return NULL ; } op = (PyBytesObject *)PyObject_MALLOC(PyBytesObject_SIZE + nbytes); if (op == NULL ) return PyErr_NoMemory(); (void )PyObject_INIT_VAR(op, &PyBytes_Type, size); op->ob_shash = -1 ; op->ob_sval[size] = '\0' ; if (Py_SIZE(a) == 1 && n > 0 ) { memset (op->ob_sval, a->ob_sval[0 ] , n); return (PyObject *) op; } i = 0 ; if (i < size) { memcpy (op->ob_sval, a->ob_sval, Py_SIZE(a)); i = Py_SIZE(a); } while (i < size) { j = (i <= size-i) ? i : size-i; memcpy (op->ob_sval+i, op->ob_sval, j); i += j; } return (PyObject *) op; }
根据索引获取指定元素:
1 2 3 4 5 6 7 >>> val = b"abcdef" >>> val[1 ], type (val[1 ])(98 , <class 'int' >) >>> >>> val[1 : 4 ], type (val[1 :4 ])(b'bcd' , <class 'bytes' >) >>>
然后我们看看底层的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static PyObject *bytes_item (PyBytesObject *a, Py_ssize_t i) { if (i < 0 || i >= Py_SIZE(a)) { PyErr_SetString(PyExc_IndexError, "index out of range" ); return NULL ; } return PyLong_FromLong((unsigned char )a->ob_sval[i]); }
那切片呢?切片的话对应bytes_subscript,但它不是在PySequenceMethods tp_as_sequence里面,而是在PyMappingMethods bytes_as_mapping里面,它是一个映射操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static PySequenceMethods bytes_as_sequence = { (lenfunc)bytes_length, (binaryfunc)bytes_concat, (ssizeargfunc)bytes_repeat, (ssizeargfunc)bytes_item, 0 , 0 , 0 , (objobjproc)bytes_contains }; static PyMappingMethods bytes_as_mapping = { (lenfunc)bytes_length, (binaryfunc)bytes_subscript, 0 , };
因为映射操作只有两个,一个是重复的,还有一个是必须要在这里说的,所以映射操作我们就放在这里介绍了。
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 static PyObject*bytes_subscript (PyBytesObject* self, PyObject* item) { if (PyIndex_Check(item)) { Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError); if (i == -1 && PyErr_Occurred()) return NULL ; if (i < 0 ) i += PyBytes_GET_SIZE(self); if (i < 0 || i >= PyBytes_GET_SIZE(self)) { PyErr_SetString(PyExc_IndexError, "index out of range" ); return NULL ; } return PyLong_FromLong((unsigned char )self->ob_sval[i]); } else if (PySlice_Check(item)) { Py_ssize_t start, stop, step, slicelength, i; size_t cur; char * source_buf; char * result_buf; PyObject* result; if (PySlice_Unpack(item, &start, &stop, &step) < 0 ) { return NULL ; } slicelength = PySlice_AdjustIndices(PyBytes_GET_SIZE(self), &start, &stop, step); if (slicelength <= 0 ) { return PyBytes_FromStringAndSize("" , 0 ); } else if (start == 0 && step == 1 && slicelength == PyBytes_GET_SIZE(self) && PyBytes_CheckExact(self)) { Py_INCREF(self); return (PyObject *)self; } else if (step == 1 ) { return PyBytes_FromStringAndSize( PyBytes_AS_STRING(self) + start, slicelength); } else { source_buf = PyBytes_AS_STRING(self); result = PyBytes_FromStringAndSize(NULL , slicelength); if (result == NULL ) return NULL ; result_buf = PyBytes_AS_STRING(result); for (cur = start, i = 0 ; i < slicelength; cur += step, i++) { result_buf[i] = source_buf[cur]; } return result; } } else { PyErr_Format(PyExc_TypeError, "byte indices must be integers or slices, not %.200s" , Py_TYPE(item)->tp_name); return NULL ; } }
所以从底层我们可以看到,Python为我们做的事情是真的不少,我们通过一个简单的切片,在底层要这么多行代码。不过在我们分析完逻辑之后,会发现其实也不过如此,毕竟逻辑很好理解。
但是在Python中,索引操作和切片操作,我们都可以通过__getitem__实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class A : def __getitem__ (self, item ): return item a = A() print (a[123 ]) print (a["name" ]) print (a[1 : 5 ]) print (a[1 : 5 : 2 ]) print (a["yo" : "ha" : "哼哼" ])
判断一个序列是否在指定的序列中:
1 2 3 4 5 6 >>> val = b"abcdef" >>> b"abc" in valTrue >>> b"cbd" in valFalse >>>
如果让你来实现的话,显然是两层for循环,那么Python是怎么做的呢?
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 static int bytes_contains (PyObject *self, PyObject *arg) { return _Py_bytes_contains(PyBytes_AS_STRING(self), PyBytes_GET_SIZE(self), arg); } _Py_bytes_contains(const char *str, Py_ssize_t len, PyObject *arg) { Py_ssize_t ival = PyNumber_AsSsize_t(arg, NULL ); if (ival == -1 && PyErr_Occurred()) { Py_buffer varg; Py_ssize_t pos; PyErr_Clear(); if (PyObject_GetBuffer(arg, &varg, PyBUF_SIMPLE) != 0 ) return -1 ; pos = stringlib_find(str, len, varg.buf, varg.len, 0 ); PyBuffer_Release(&varg); return pos >= 0 ; } if (ival < 0 || ival >= 256 ) { PyErr_SetString(PyExc_ValueError, "byte must be in range(0, 256)" ); return -1 ; } return memchr (str, (int ) ival, len) != NULL ; }
效率问题 我们知道Python中对于不可变对象运算的处理方式就是,再创建一个新的。所以三个bytes对象a、b、c相加时,那么会先根据a + b创建新的临时对象,然后再根据”临时对象+c”创建新的对象,返回指针。所以:
1 2 3 result = b"" for _ in bytes_list: result += _
这是一种效率非常低下的做法,因为涉及大量临时对象的创建和销毁,不仅是这里bytes,后面即将分析的字符串也是同样的道理。官方推荐的做法是,使用join,字符串和字节序列都可以对一个列表进行join,将列表里面的多个字符串或者字节序列join在一起。
举个Python中的例子,我们以字符串为例,字节序列同样如此:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def bad (): s = "" for _ in range (1 , 10 ): s += str (_) return s def good (): l = [] for _ in range (1 , 10 ): l.append(str (_)) return "" .join(l) def better (): return "" .join(str (_) for _ in range (1 , 10 )) def best (): return "" .join(map (str , range (1 , 10 )))
+号在拼接字符串的时候,会先开辟一块空间,把+号左边的字符串和+号右边的字符串合起来放在新开辟的空间内,这样有几个+号,就会开辟几次空间。 加号操作符
join拼接字符串也需要开辟空间,但是会提前算好总共需要开辟多大的空间,一次性把内存申请好,然后把所有的字符串合起来放在这个新的空间中,这样就不用频繁申请空间了。节约空间又节约时间。 join连接
从上面的例子中,可以看到join比+少申请了一块a+b的空间。
字节序列缓冲池 为了优化单字节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 39 40 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 _Py_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); if (size == 1 ) { characters[*str & UCHAR_MAX] = op; Py_INCREF(op); } return (PyObject *) op; }
由此可见,当 Python 程序开始运行时,字符缓冲池是空的。随着单字节 bytes*对象的创建,缓冲池中的对象慢慢多了起来。
这样一来,字符对象首次创建后便在缓冲池中缓存起来;后续再次使用时, Python 直接从缓冲池中取,避免重复创建和销毁。与前面章节介绍的小整数对象池一样,字符对象只有为数不多的 256 个,但使用频率非常高。缓冲池技术作为一种以时间换空间的优化手段,只需较小的内存为代价,便可明显提升执行效率。
1 2 3 4 5 6 7 8 9 10 >>> a1 = b"a" >>> a2 = b"a" >>> a1 is a2True >>> >>> a1 = b"ab" >>> a2 = b"ab" >>> a1 is a2False >>>
显然此时不需要我解释了,单字节bytes对象会缓存起来,不是单字节则不会缓存。
bytearray对象 除了bytes对象之外,Python中还有一个bytearray对象,它和bytes对象类似,只不过bytes对象是不可变的,而bytearray对象是可变的。所以就不单独分析了,这里简单提一嘴。
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 s = bytearray ([99 , 100 , 101 ]) print (s) s = bytearray (b"abc" ) print (s)s = bytearray ("古明地觉" , encoding="utf-8" ) print (s) print (s.decode("utf-8" )) s[-3 :] = "恋" .encode("utf-8" ) print (s) print (s.decode("utf-8" )) s = bytearray (b"abc" ) print (s[0 ], s[1 ], s[2 ]) print (s[:2 ]) print (b"--" .join([bytearray (b"abc" ), bytearray (b"def" )]))
因此把bytearray对象想象成可变的bytes对象即可,它的使用和bytes对象非常类似,一些操作的行为也是一样的,所以就不单独分析了,下一篇将会分析Python中的字符串。
小结 这次我们分析了bytes对象的底层实现,我们说:
bytes对象是一个变长、不可变对象,内部的值是通过一个C的字符数组来维护的;
bytes也是序列型操作,它支持的操作在bytes_as_sequence中;
Python内部维护字符缓冲池来优化单字节bytes对象的创建和销毁操作;
缓冲池是一种常用的以空间换时间的优化技术;