Python內(nèi)建類(lèi)型bytes深入理解
引言
“深入認(rèn)識(shí)Python內(nèi)建類(lèi)型”這部分的內(nèi)容會(huì)從源碼角度為大家介紹Python中各種常用的內(nèi)建類(lèi)型。
在我們?nèi)粘5拈_(kāi)發(fā)中,str是很常用的一個(gè)內(nèi)建類(lèi)型,與之相關(guān)的我們比較少接觸的就是bytes,這里先為大家介紹一下bytes相關(guān)的知識(shí)點(diǎn),下一篇博客再詳細(xì)介紹str的相關(guān)內(nèi)容。
1 bytes和str之間的關(guān)系
不少語(yǔ)言中的字符串都是由字符數(shù)組(或稱(chēng)為字節(jié)序列)來(lái)表示的,例如C語(yǔ)言:
char str[] = "Hello World!";
由于一個(gè)字節(jié)最多只能表示256種字符,要想覆蓋眾多的字符(例如漢字),就需要通過(guò)多個(gè)字節(jié)來(lái)表示一個(gè)字符,即多字節(jié)編碼。但由于原始字節(jié)序列中沒(méi)有維護(hù)編碼信息,操作不慎就很容易導(dǎo)致各種亂碼現(xiàn)象。
Python提供的解決方法是使用Unicode對(duì)象(也就是str對(duì)象),Unicode口語(yǔ)表示各種字符,無(wú)需關(guān)心編碼。但是在存儲(chǔ)或者網(wǎng)絡(luò)通訊時(shí),字符串對(duì)象需要序列化成字節(jié)序列。為此,Python額外提供了字節(jié)序列對(duì)象——bytes。
str和bytes的關(guān)系如圖所示:

str對(duì)象統(tǒng)一表示一個(gè)字符串,不需要關(guān)心編碼;計(jì)算機(jī)通過(guò)字節(jié)序列與存儲(chǔ)介質(zhì)和網(wǎng)絡(luò)介質(zhì)打交道,字節(jié)序列用bytes對(duì)象表示;存儲(chǔ)或傳輸str對(duì)象時(shí),需要將其序列化成字節(jié)序列,序列化過(guò)程也是編碼的過(guò)程。
2 bytes對(duì)象的結(jié)構(gòu):PyBytesObject
C源碼:
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;
源碼分析:
字符數(shù)組ob_sval存儲(chǔ)對(duì)應(yīng)的字符,但是ob_sval數(shù)組的長(zhǎng)度并不是ob_size,而是ob_size + 1.這是Python為待存儲(chǔ)的字節(jié)序列額外分配了一個(gè)字節(jié),用于在末尾處保存’\0’,以便兼容C字符串。
ob_shash:用于保存字節(jié)序列的哈希值。由于計(jì)算bytes對(duì)象的哈希值需要遍歷其內(nèi)部的字符數(shù)組,開(kāi)銷(xiāo)相對(duì)較大。因此Python選擇將哈希值保存起來(lái),以空間換時(shí)間(隨處可見(jiàn)的思想,hh),避免重復(fù)計(jì)算。
圖示如下:

3 bytes對(duì)象的行為
3.1 PyBytes_Type
C源碼:
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 */
// ...
};
數(shù)值型操作bytes_as_number:
static PyNumberMethods bytes_as_number = {
0, /*nb_add*/
0, /*nb_subtract*/
0, /*nb_multiply*/
bytes_mod, /*nb_remainder*/
};
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對(duì)象只是借用%運(yùn)算符實(shí)現(xiàn)字符串格式化,并不是真正意義上的數(shù)值運(yùn)算(這里其實(shí)和最開(kāi)始的分類(lèi)標(biāo)準(zhǔn)是有點(diǎn)歧義的,按標(biāo)準(zhǔn)應(yīng)該再分一個(gè)“格式型操作”,不過(guò)靈活處理也是必須的):
>>> b'msg: a = %d, b = %d' % (1, 2) b'msg: a = 1, b = 2'
序列型操作bytes_as_sequence:
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個(gè):
- bytes_length:查詢序列長(zhǎng)度
- bytes_concat:將兩個(gè)序列合并為一個(gè)
- bytes_repeat:將序列重復(fù)多次
- bytes_item:取出給定下標(biāo)的序列元素
- bytes_contains:包含關(guān)系判斷
關(guān)聯(lián)型操作bytes_as_mapping:
static PyMappingMethods bytes_as_mapping = {
(lenfunc)bytes_length,
(binaryfunc)bytes_subscript,
0,
};
可以看到bytes支持獲取長(zhǎng)度和切片兩個(gè)操作。
3.2 bytes_as_sequence
這里我們主要介紹以下bytes_as_sequence相關(guān)的操作
bytes_as_sequence中的操作都不復(fù)雜,但是會(huì)有一個(gè)“陷阱”,這里我們以bytes_concat操作來(lái)認(rèn)識(shí)一下這個(gè)問(wèn)題。C源碼如下:
/* 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;
}
bytes_concat源碼大家可自行分析,這里直接以圖示形式來(lái)展示,主要是為了說(shuō)明其中的“陷阱”。
圖示如下:

- Py_buffer提供了一套操作對(duì)象緩沖區(qū)的統(tǒng)一接口,屏蔽不同類(lèi)型對(duì)象的內(nèi)部差異
- bytes_concat則將兩個(gè)對(duì)象的緩沖區(qū)拷貝到一起,形成新的bytes對(duì)象
上述的拷貝過(guò)程是比較清晰的,但是這里隱藏著一個(gè)問(wèn)題——數(shù)據(jù)拷貝的陷阱。
以合并3個(gè)bytes對(duì)象為例:
>>> a = b'abc' >>> b = b'def' >>> c = b'ghi' >>> result = a + b + c >>> result b'abcdefghi'
本質(zhì)上這個(gè)過(guò)程會(huì)合并兩次
>>> t = a + b >>> result = t + c
在這個(gè)過(guò)程中,a和b的數(shù)據(jù)都會(huì)被拷貝兩遍,圖示如下:

不難推出,合并n個(gè)bytes對(duì)象,頭兩個(gè)對(duì)象需要拷貝n - 1次,只有最后一個(gè)對(duì)象不需要重復(fù)拷貝,平均下來(lái)每個(gè)對(duì)象大約要拷貝n/2次。因此,下面的代碼:
>>> result = b''
>>> for b in segments:
result += s
效率是很低的。我們可以使用join()來(lái)優(yōu)化:
>>> result = b''.join(segments)
join()方法是bytes對(duì)象提供的一個(gè)內(nèi)建方法,可以高效合并多個(gè)bytes對(duì)象。join方法對(duì)數(shù)據(jù)拷貝進(jìn)行了優(yōu)化:先遍歷待合并對(duì)象,計(jì)算總長(zhǎng)度;然后根據(jù)總長(zhǎng)度創(chuàng)建目標(biāo)對(duì)象;最后再遍歷待合并對(duì)象,逐一拷貝數(shù)據(jù)。這樣一來(lái),每個(gè)對(duì)象只需要拷貝一次,解決了重復(fù)拷貝的陷阱。(具體源碼大家可以自行去查看)
4 字符緩沖池
和小整數(shù)一樣,字符對(duì)象(即單字節(jié)的bytes對(duì)象)數(shù)量也很少,只有256個(gè),但使用頻率非常高,因此以空間換時(shí)間能明顯提升執(zhí)行效率。字符緩沖池源碼如下:
static PyBytesObject *characters[UCHAR_MAX + 1];
下面我們從創(chuàng)建bytes對(duì)象的過(guò)程來(lái)看一下字符緩沖池的使用:PyBytes_FromStringAndSize()函數(shù)是負(fù)責(zé)創(chuàng)建bytes對(duì)象的通用接口,源碼如下:
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;
}
其中涉及字符緩沖區(qū)維護(hù)的關(guān)鍵步驟如下:
第10~17行:如果創(chuàng)建的對(duì)象為單字節(jié)對(duì)象,會(huì)先在characters數(shù)組的對(duì)應(yīng)序號(hào)判斷是否已經(jīng)有相應(yīng)的對(duì)象存儲(chǔ)在了緩沖區(qū)中,如果有則直接取出
第28~31行:如果創(chuàng)建的對(duì)象為單字節(jié)對(duì)象,并且之前已經(jīng)判斷了不在緩沖區(qū)中,則將其放入字符緩沖池的對(duì)應(yīng)位置
由此可見(jiàn),當(dāng)Python程序開(kāi)始運(yùn)行時(shí),字符緩沖池是空的。隨著單字節(jié)bytes對(duì)象的創(chuàng)建,緩沖池中的對(duì)象就慢慢多了起來(lái)。當(dāng)緩沖池已緩存b’1’、b’2’、b’3’、b’a’、b’b’、b’c’這幾個(gè)字符時(shí),內(nèi)部結(jié)構(gòu)如下:

示例:
注:這里大家可能在IDLE和PyCharm中獲得的結(jié)果不一致,這個(gè)問(wèn)題在之前的博客中也提到過(guò),查閱資料后得到的結(jié)論是:IDLE運(yùn)行和PyCharm運(yùn)行的方式不同。這里我將PyCharm代碼對(duì)應(yīng)的代碼對(duì)象反編譯的結(jié)果展示給大家,但我對(duì)IDLE的認(rèn)識(shí)還比較薄弱,以后有機(jī)會(huì)再給大家詳細(xì)補(bǔ)充這個(gè)知識(shí)(抱拳~)。
這里大家還是先以認(rèn)識(shí)字符緩沖區(qū)這個(gè)概念為主,當(dāng)然字節(jié)碼的相關(guān)知識(shí)掌握好了也是很有幫助的。以下是PyCharm運(yùn)行的結(jié)果:
以下操作的相關(guān)講解可以看這篇博客:Python程序執(zhí)行過(guò)程與字節(jié)碼
示例1:


下面我們來(lái)看一下反編譯的結(jié)果:(下面的文件路徑我省略了,大家自己試驗(yàn)的時(shí)候要輸入正確的路徑)
>>> text = open('D:\\...\\test2.py').read()
>>> result= compile(text,'D:\\...\\test2.py', 'exec')
>>> import dis
>>> dis.dis(result)
1 0 LOAD_CONST 0 (b'a')
2 STORE_NAME 0 (a)
2 4 LOAD_CONST 0 (b'a')
6 STORE_NAME 1 (b)
3 8 LOAD_NAME 2 (print)
10 LOAD_NAME 0 (a)
12 LOAD_NAME 1 (b)
14 IS_OP 0
16 CALL_FUNCTION 1
18 POP_TOP
20 LOAD_CONST 1 (None)
22 RETURN_VALUE
可以很清晰地看到,第5行和第8行的LOAD_CONST指令操作的都是下標(biāo)為0的常量b’a’,因此此時(shí)a和b對(duì)應(yīng)的是同一個(gè)對(duì)象,我們打印看一下:
>>> result.co_consts[0] b'a'
示例2:
為了確認(rèn)只會(huì)緩存單字節(jié)的bytes對(duì)象,我在這里又嘗試了多字節(jié)的bytes對(duì)象,同樣還是在PyCharm環(huán)境下嘗試:


結(jié)果是比較出乎意料的:多字節(jié)的bytes對(duì)象依然是同一個(gè)。為了驗(yàn)證這個(gè)想法,我們先來(lái)看一下對(duì)代碼對(duì)象的反編譯結(jié)果:
>>> text = open('D:\\...\\test3.py').read()
>>> result= compile(text,'D:\\...\\test3.py', 'exec')
>>> import dis
>>> dis.dis(result)
1 0 LOAD_CONST 0 (b'abc')
2 STORE_NAME 0 (a)
2 4 LOAD_CONST 0 (b'abc')
6 STORE_NAME 1 (b)
3 8 LOAD_NAME 2 (print)
10 LOAD_NAME 0 (a)
12 LOAD_NAME 1 (b)
14 IS_OP 0
16 CALL_FUNCTION 1
18 POP_TOP
20 LOAD_CONST 1 (None)
22 RETURN_VALUE
>>> result.co_consts[0]
b'abc'
可以看到,反編譯的結(jié)果和單字節(jié)的bytes對(duì)象沒(méi)有區(qū)別。。。
(TODO:這里我嘗試去看了PyBytes_FromStringAndSize()中相關(guān)的其他調(diào)用,但是由于水平有限,沒(méi)有找到這個(gè)問(wèn)題的解釋?zhuān)@個(gè)問(wèn)題先暫時(shí)放下,隨著理解源碼更深刻再繼續(xù)解決)
以上就是Python內(nèi)建類(lèi)型bytes深入理解的詳細(xì)內(nèi)容,更多關(guān)于Python內(nèi)建類(lèi)型bytes的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Python3中的bytes類(lèi)型和str類(lèi)型
- Python中的bytes類(lèi)型用法及實(shí)例分享
- python數(shù)據(jù)類(lèi)型bytes?和?bytearray的使用與區(qū)別
- 簡(jiǎn)單了解Python3 bytes和str類(lèi)型的區(qū)別和聯(lián)系
- python中bytes和str類(lèi)型的區(qū)別
- Python3中的bytes和str類(lèi)型詳解
- 對(duì)python的bytes類(lèi)型數(shù)據(jù)split分割切片方法
- Python3中bytes類(lèi)型轉(zhuǎn)換為str類(lèi)型
- Python字節(jié)串類(lèi)型bytes及用法
相關(guān)文章
一文詳述 Python 中的 property 語(yǔ)法
這篇文章主要介紹了一文詳述 Python 中的 property 語(yǔ)法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
Python常用隨機(jī)數(shù)與隨機(jī)字符串方法實(shí)例
這篇文章主要介紹了Python常用隨機(jī)數(shù)與隨機(jī)字符串方法實(shí)例,本文講解了隨機(jī)整數(shù)、隨機(jī)選取0到100間的偶數(shù)、隨機(jī)浮點(diǎn)數(shù)、隨機(jī)字符串等常用隨機(jī)方法,需要的朋友可以參考下2015-04-04
tensorflow之讀取jpg圖像長(zhǎng)和寬實(shí)例
這篇文章主要介紹了tensorflow之讀取jpg圖像長(zhǎng)和寬實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-06-06
Python一行代碼識(shí)別車(chē)牌號(hào)碼實(shí)現(xiàn)示例詳解
這篇文章主要為大家介紹了Python一行代碼識(shí)別車(chē)牌號(hào)碼實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
python openpyxl方法 zip函數(shù)用法及說(shuō)明
這篇文章主要介紹了python openpyxl方法 zip函數(shù)用法及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
python3應(yīng)用windows api對(duì)后臺(tái)程序窗口及桌面截圖并保存的方法
今天小編就為大家分享一篇python3應(yīng)用windows api對(duì)后臺(tái)程序窗口及桌面截圖并保存的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-08-08
Python利用itchat模塊定時(shí)給朋友發(fā)送微信信息
這篇文章主要介紹了在Python中利用itchat模塊編寫(xiě)一個(gè)爬蟲(chóng)腳本,可以實(shí)現(xiàn)每天定時(shí)給朋友發(fā)微信暖心話,感興趣的可以跟隨小編一起學(xué)習(xí)一下2022-01-01
Python語(yǔ)言的12個(gè)基礎(chǔ)知識(shí)點(diǎn)小結(jié)
這篇文章主要介紹了Python語(yǔ)言的12個(gè)基礎(chǔ)知識(shí)點(diǎn)小結(jié),包含正則表達(dá)式替換、遍歷目錄方法、列表按列排序、去重、字典排序等,需要的朋友可以參考下2014-07-07

