楔子
不少編程語言中的"字符串"都是使用字符數組(或者稱字符序列)來表示,比如C語言和go語言就是這樣。
char name[] = "komeiji satori";
一個字節最多能表示256個字符,所以對於英文來說足夠了,因此一個英文字符占一個字節即可,然而對於那些非英文字符便力不從心了。因此為了表示這些非英文編碼,於是多字節編碼應運而生----通過多個字節來表示一個字符。但由於原始字節序列不維護編碼信息,因此操作不慎便導致各種亂碼現象。
而Python提供的解決方案是使用unicode(在Python3中等價於str)
表示字符串,因為unicode可以表示各種字符,不需要關心編碼的問題。但在存儲或網絡通訊時,字符串不可避免地要序列化成字節序列。為此,Python除了提供字符串對象之外,還額外提供了字節序列對象----bytes。
如上圖,str對象統一表示一個字符串,不需要關心編碼;計算機通過字節序列和存儲介質、網絡介質打交道,字節序列由bytes對象表示;在存儲和傳輸str對象的時候,需要將其序列化成字節序列,序列化也是編碼的過程。
下面我們就來看看bytes對象在底層的數據結構。
PyBytesObject
我們說bytes對象是由若干個字節組成的,顯然這是一個變長對象,有多少個字節說明其長度是多少。
//Include/bytesobject.h
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;
我們看一下里面的成員對象:
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也是可以的。
>>> val = b""
>>> sys.getsizeof(val)
33
>>>
val = b"abc"
>>> val = b"abc"
>>> sys.getsizeof(val)
36 # 32 + 4
>>>
bytes對象的行為
介紹bytes對象在底層的數據結構之后,我們要考察bytes對象的行為。我們說實例對象的行為由其類型對象決定,所以bytes對象具有哪些行為,就看bytes類型對象本身定義了哪些操作。bytes類型對象,顯然對應PyBytes_Type,根據我們之前介紹的規律,也可以猜出來,它定義在Object/bytesobject.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 */
// ...
};
到了現在,相信你對類型對象的結構肯定非常熟悉了,因為類型對象都是由PyTypeObject結構體實例化得到的。我們看到tp_as_number,它居然不是0,而是傳遞了一個指針,說明確實指向了一個PyNumberMethods結構體實例。難道bytes支持數值運算,這顯然是不可能的啊,所以我們需要進入bytes_as_number中一探究竟。
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對象只是借用了%運算實現了格式化,談不上數值運算,虛驚一場。不過由此也看到了Python的動態特性,即使是相同的操作,但如果是不同類型的對象執行的話,也會有不同的表現。
>>> 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的定義。
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:根據索引獲取指定的下表, 得到一個整型;如果是切片,那么還會得到一個bytes對象
sq_contains:判斷某個序列是不是在該序列中,顯然它等價於Python中的in操作
查看序列長度:
顯然這是最簡單的,直接獲取ob_size即可,比如:val = b"abcde",那么長度就是5。
static Py_ssize_t
bytes_length(PyBytesObject *a)
{
return Py_SIZE(a);
}
將兩個序列合並為一個:
>>> a = b"abc"
>>> b = b"def"
>>> a + b
b'abcdef'
>>>
而且我們看到這里相當於是加法運算,我們很容易想到會是PyNumberMethods中的nb_add,比如:PyLongObject對應的long_add、PyFloatObject對應的float_add,但對於bytes對象而言,加法操作對應PySequenceMethods的sq_concat。所以我們看到Python中的同一個操作符,在底層會對應不同的函數,比如:long_add和float_add、以及這里的bytes_concat,在Python的層面都是+這個操作符。然后我們看看底層是怎么對兩個字節序列進行相加的。
static PyObject *
bytes_concat(PyObject *a, PyObject *b)
{
//兩個局部變量,用於維護緩沖區
Py_buffer va, vb;
//result用於保存結果
PyObject *result = NULL;
//將緩沖區的長度設置為-1, 可以認為此時緩沖區啥也沒有
va.len = -1;
vb.len = -1;
//將a、b中ob_sval拷貝到緩沖區中,拷貝成功返回0,拷貝失敗返回非0
//如果下面的條件不成功, 就意味着拷貝失敗了, 說明至少有一個老鐵不是bytes類型
if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 ||
PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0) {
//然后設置異常,PyExc_TypeError表示TypeError(類型錯誤),專門用來指對一個對象執行了它所不支持的操作
PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s",
Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name);
//比如:"123" + 123, 會得到: TypeError: can't concat int to bytes, 和這里設置的異常信息是一樣的
//這里直接跳轉到done
goto done;
}
//這里是判斷是否有一方長度為0, 如果a長度為0,那么相加之后結果就是b
if (va.len == 0 && PyBytes_CheckExact(b)) {
//將b拷貝給result
result = b;
//增加result的引用計數
Py_INCREF(result);
//跳轉
goto done;
}
//和上面同理,如果b長度為0,那么相加之后的結果就是a
if (vb.len == 0 && PyBytes_CheckExact(a)) {
//將a拷貝給result
result = a;
//增加引用計數
Py_INCREF(result);
//跳轉
goto done;
}
//這里是判斷兩個字節序列合並之后,長度是否超過限制,因為不允許超過PY_SSIZE_T_MAX
//所以更直觀的寫法應該是 if (va.len + vb.len > PY_SSIZE_T_MAX), 但是這個條件基本不可能滿足,除非你寫惡意代碼
if (va.len > PY_SSIZE_T_MAX - vb.len) {
PyErr_NoMemory();
goto done;
}
//否則話,聲明指定容量PyBytesObject
result = PyBytes_FromStringAndSize(NULL, va.len + vb.len);
if (result != NULL) {
//將緩沖區va里面內容拷貝到result的ob_sval中,拷貝的長度為va.len
//PyBytes_AS_STRING是一個宏,用於獲取PyBytesObject中的ob_sval
memcpy(PyBytes_AS_STRING(result), va.buf, va.len);
//然后將緩沖區vb里面的內容拷貝到result的ob_sval中,拷貝的長度為vb.len,但是從va.len的位置開始拷貝, 不然會把內容覆蓋掉
memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);
}
done:
//如果長度不會-1,那么要將緩沖區里面的內容釋放掉,否則可能導致內存泄漏
if (va.len != -1)
PyBuffer_Release(&va);
if (vb.len != -1)
PyBuffer_Release(&vb);
//返回result
return result;
}
雖然代碼很長,但是不難理解。不過可能有人認為為什么非要先將a、b的內容拷貝到Py_buffer里面,再通過Py_buffer拷貝到result里面去呢?直接拷貝不可以嗎?答案是Py_buffer提供了一套操作對象緩沖區的統一接口,屏蔽不同類型對象的內部差異。
將序列重復多次:
>>> a = b"abc"
>>> a * 3
b'abcabcabc'
>>> a * -1
b'' # 如果乘上一個負數,等於乘上0,那么會得到一個空的字節序列
>>>
然后我們看看底層的實現:
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;
//如果n小於0, 那么等於0
if (n < 0)
n = 0;
//這里條件寫成Py_SIZE(a) * n > PY_SSIZE_T_MAX更容易理解
if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n) {
//先計算相乘之后字節序列的長度是否超過最大限制,如果超過了,直接報錯
PyErr_SetString(PyExc_OverflowError,
"repeated bytes are too long");
return NULL;
}
//計算Py_SIZE(a) * n得到size
size = Py_SIZE(a) * n;
if (size == Py_SIZE(a) && PyBytes_CheckExact(a)) {
//如果兩者相等,那么證明n = 1,直接增加引用計數,然后返回a即可
Py_INCREF(a);
return (PyObject *)a;
}
//類型轉化,此時是size_t類型,相當於無符號64位整型
nbytes = (size_t)size;
//PyBytesObject_SIZE是一個宏,表示PyBytesObject的基本大小
//它是一個宏,等價於(offsetof(PyBytesObject, ob_sval) + 1), 顯然是33
//所以nbytes + PyBytesObject_SIZE就是bytes對象所需要的空間
//如果nbytes + PyBytesObject_SIZE還小於等於nbytes, 所以相加之后size_t類型存不下了
//說明超過所占內存的極限了
if (nbytes + PyBytesObject_SIZE <= nbytes) {
PyErr_SetString(PyExc_OverflowError,
"repeated bytes are too long");
return NULL;
}
//申請空間,大小為PyBytesObject_SIZE + nbytes
op = (PyBytesObject *)PyObject_MALLOC(PyBytesObject_SIZE + nbytes);
if (op == NULL)
//返回NULL,表示申請失敗
return PyErr_NoMemory();
//PyObject_INIT_VAR是一個宏,設置ob_type和ob_size
(void)PyObject_INIT_VAR(op, &PyBytes_Type, size);
//設置ob_shash為-1
op->ob_shash = -1;
//將ob_sval最后一位設置為'\0'
op->ob_sval[size] = '\0';
if (Py_SIZE(a) == 1 && n > 0) {
//顯然這里是在a對應的bytes對象長度為1時,所走的邏輯
//直接將op->ob_sval里面元素設置a->ob_sval[0], 設置n個
memset(op->ob_sval, a->ob_sval[0] , n);
return (PyObject *) op;
}
i = 0;
//否則將a -> ob_sval拷貝到op -> ob_sval中, 拷貝n次, 因為size = Py_SIZE(a) * n;
//這里是先拷貝了一次
if (i < size) {
memcpy(op->ob_sval, a->ob_sval, Py_SIZE(a));
i = Py_SIZE(a);
}
//然后拷貝n - 1次
while (i < size) {
j = (i <= size-i) ? i : size-i;
memcpy(op->ob_sval+i, op->ob_sval, j);
i += j;
}
return (PyObject *) op;
}
根據索引獲取指定元素:
>>> val = b"abcdef"
>>> val[1], type(val[1])
(98, <class 'int'>)
>>>
>>> val[1: 4], type(val[1:4])
(b'bcd', <class 'bytes'>)
>>>
然后我們看看底層的實現:
static PyObject *
bytes_item(PyBytesObject *a, Py_ssize_t i)
{
//如果i < 0或者 i >= a的ob_size,那么會報錯:索引越界
//但是我們記得Python支持負數索引的啊,是的,只不過會手動幫你變成正的
//因為C是不支持負數索引的,所以通過C的索引獲取,那么索引一定是正的
//因此我們填上的負數,Python會幫你加上長度。比如:長度為5,但是我們寫的索引為-1, 那么Python會幫你變成4之后再獲取
if (i < 0 || i >= Py_SIZE(a)) {
PyErr_SetString(PyExc_IndexError, "index out of range");
return NULL;
}
//我們看到獲取第i個元素之后直接轉成了PyLongObject,然后返回指針
return PyLong_FromLong((unsigned char)a->ob_sval[i]);
}
那切片呢?切片的話對應bytes_subscript,但它不是在PySequenceMethods tp_as_sequence里面,而是在PyMappingMethods bytes_as_mapping里面,它是一個映射操作。
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對象中只有兩個,一個bytes_length獲取長度,這個在bytes_as_sequence中已經實現了,還有一個就是bytes_subscript進行切片操作
static PyMappingMethods bytes_as_mapping = {
(lenfunc)bytes_length,
(binaryfunc)bytes_subscript,
0,
};
因為映射操作只有兩個,一個是重復的,還有一個是必須要在這里說的,所以映射操作我們就放在這里介紹了。
static PyObject*
bytes_subscript(PyBytesObject* self, PyObject* item)
{
//參數是self和item,那么在Python的層面上就類似於self[item]
//檢測item,看它是不是一個整型
if (PyIndex_Check(item)) {
//如果是轉成Ssize_t
Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError);
if (i == -1 && PyErr_Occurred())
return NULL;
//如果i小於0,那么將i加上序列的長度,得到正數索引
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;
//這里是會將item解包
if (PySlice_Unpack(item, &start, &stop, &step) < 0) {
return NULL;
}
//得到拷貝的字節個數比如:ob_sval長度為9, 但是未必拷貝9個,所以這個slicelength是計算的拷貝的字節個數
slicelength = PySlice_AdjustIndices(PyBytes_GET_SIZE(self), &start,
&stop, step);
//slicelength小於等於0的話,直接返回空的字節序列,比如val[3: 2],顯然此時是不循環的,因為start對應的位置在end之后,而且步長為正
if (slicelength <= 0) {
return PyBytes_FromStringAndSize("", 0);
}
//如果起始位置為0,步長為1,且拷貝的字節個數等於字節序列的長度
else if (start == 0 && step == 1 &&
slicelength == PyBytes_GET_SIZE(self) &&
PyBytes_CheckExact(self)) {
//那么增加引用計數,直接返回
Py_INCREF(self);
return (PyObject *)self;
}
else if (step == 1) {
//如果步長是1,那么從start開始拷貝,拷貝slicelength個字字節
return PyBytes_FromStringAndSize(
PyBytes_AS_STRING(self) + start,
slicelength);
}
else {
//走到這里,說明步長不是1,只能一個一個拷貝了
source_buf = PyBytes_AS_STRING(self);
//創建PyBytesObject對象,空間為slicelength
result = PyBytes_FromStringAndSize(NULL, slicelength);
if (result == NULL)
return NULL;
//拿到內部的ob_sval
result_buf = PyBytes_AS_STRING(result);
//從start開始然后一個字節一個字節的拷貝過去
//start開始拷貝,依舊循環slicelength,通過cur記錄拷貝的位置,然后每次循環都加上步長step
for (cur = start, i = 0; i < slicelength;
cur += step, i++) {
result_buf[i] = source_buf[cur];
}
//返回
return result;
}
}
//item要么是整數、要么是切片,走到這里說明不滿足條件
else {
//比如:item我們傳遞了一個字符串,顯然此時在通過這種方式獲取的話,這屬於字典的操作
//所以拋出TypeError異常
PyErr_Format(PyExc_TypeError,
"byte indices must be integers or slices, not %.200s",
Py_TYPE(item)->tp_name);
//返回空
return NULL;
}
}
所以從底層我們可以看到,Python為我們做的事情是真的不少,我們通過一個簡單的切片,在底層要這么多行代碼。不過在我們分析完邏輯之后,會發現其實也不過如此,畢竟邏輯很好理解。
但是在Python中,索引操作和切片操作,我們都可以通過__getitem__實現。
class A:
def __getitem__(self, item):
return item
a = A()
print(a[123]) # 123
print(a["name"]) # name
print(a[1: 5]) # slice(1, 5, None)
print(a[1: 5: 2]) # slice(1, 5, 2)
print(a["yo": "ha": "哼哼"]) # slice('yo', 'ha', '哼哼')
# 通過__getitem__,我們可以同時實現切片、索引獲取,但是當item為字符串時,我們還可以實現字典操作
# 當然這部分內容,我們會在后面系列中分析類的時候介紹。
判斷一個序列是否在指定的序列中:
>>> val = b"abcdef"
>>> b"abc" in val
True
>>> b"cbd" in val
False
>>>
如果讓你來實現的話,顯然是兩層for循環,那么Python是怎么做的呢?
static int
bytes_contains(PyObject *self, PyObject *arg)
{
//比如: b"abc" in b"abcde"會調用這里的bytes_contains
//self就是b"abcde"對應的PyBytesObject的指針,arg是b"abc"對應的PyBytesObject的指針
//顯然這里調用了_Py_bytes_contains, 傳入了self -> ob_sval, self -> ob_size, arg
return _Py_bytes_contains(PyBytes_AS_STRING(self), PyBytes_GET_SIZE(self), arg);
}
//上面的源碼沒有說明,顯然是在bytesobject.c中
//但是_Py_bytes_contains位於bytes_methods.c中
_Py_bytes_contains(const char *str, Py_ssize_t len, PyObject *arg)
{
//將arg轉成整型, 但是顯然只有當arg -> ob_savl的有效字節為1時才可以這么做
Py_ssize_t ival = PyNumber_AsSsize_t(arg, NULL);
if (ival == -1 && PyErr_Occurred()) {
//所以如果ival == -1 && PyErr_Occurred(),說明arg -> ob_sval的有效字節數大於1
Py_buffer varg;//緩沖區
Py_ssize_t pos;//遍歷位置
PyErr_Clear();//這里將異常清空
//將arg -> ob_sval設置到緩存區中
if (PyObject_GetBuffer(arg, &varg, PyBUF_SIMPLE) != 0)
return -1;
//調用stringlib_find找到其位置,里面也是使用了循環
pos = stringlib_find(str, len,
varg.buf, varg.len, 0);
PyBuffer_Release(&varg); //釋放緩沖區
//如果pos大於0確實找到了,否則返回-1
return pos >= 0;
}
//否則說明字節不合法
if (ival < 0 || ival >= 256) {
PyErr_SetString(PyExc_ValueError, "byte must be in range(0, 256)");
return -1;
}
//走到這里說明是單個字節,直接調用C中memchr去尋找即可
return memchr(str, (int) ival, len) != NULL;
}
效率問題
我們知道Python中對於不可變對象運算的處理方式就是,再創建一個新的。所以三個bytes對象a、b、c相加時,那么會先根據a + b創建新的臨時對象,然后再根據"臨時對象+c"創建新的對象,返回指針。所以:
result = b""
for _ in bytes_list:
result += _
這是一種效率非常低下的做法,因為涉及大量臨時對象的創建和銷毀,不僅是這里bytes,后面即將分析的字符串也是同樣的道理。官方推薦的做法是,使用join,字符串和字節序列都可以對一個列表進行join,將列表里面的多個字符串或者字節序列join在一起。
舉個Python中的例子,我們以字符串為例,字節序列同樣如此:
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)))
字節序列緩沖池
為了優化單字節bytes對象的創建效率,Python底層內部維護了一個緩沖池。
static PyBytesObject *characters[UCHAR_MAX + 1];
Python內部創建單字節bytes對象時,先檢查目標對象是否已在緩沖池中。PyBytes_FromStringAndSize函數是負責創建bytes對象的通用接口,同樣位於 Objects/bytesobject.c 中:
PyObject *
PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)
{
//PyBytesObject對象的指針
PyBytesObject *op;
if (size < 0) {
//顯然size不可以小於0
PyErr_SetString(PyExc_SystemError,
"Negative size passed to PyBytes_FromStringAndSize");
return NULL;
}
//如果size為1表名創建的是單字節對象,當然str不可以為NULL, 而且獲取到的字節必須要在characters里面
if (size == 1 && str != NULL &&
(op = characters[*str & UCHAR_MAX]) != NULL)
{
#ifdef COUNT_ALLOCS
_Py_one_strings++;
#endif
//增加引用計數,返回指針
Py_INCREF(op);
return (PyObject *)op;
}
//否則話創建新的PyBytesObject,此時是個空
op = (PyBytesObject *)_PyBytes_FromSize(size, 0);
if (op == NULL)
return NULL;
if (str == NULL)
return (PyObject *) op;
//不管size是對少,都直接拷貝即可
memcpy(op->ob_sval, str, size);
//但是size是1的話,除了拷貝還會放到緩存池characters中
if (size == 1) {
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
//返回其指針
return (PyObject *) op;
}
由此可見,當 Python 程序開始運行時,字符緩沖池是空的。隨着單字節 bytes*對象的創建,緩沖池中的對象慢慢多了起來。
這樣一來,字符對象首次創建后便在緩沖池中緩存起來;后續再次使用時, Python 直接從緩沖池中取,避免重復創建和銷毀。與前面章節介紹的小整數對象池一樣,字符對象只有為數不多的 256 個,但使用頻率非常高。緩沖池技術作為一種以時間換空間的優化手段,只需較小的內存為代價,便可明顯提升執行效率。
>>> a1 = b"a"
>>> a2 = b"a"
>>> a1 is a2
True
>>>
>>> a1 = b"ab"
>>> a2 = b"ab"
>>> a1 is a2
False
>>>
顯然此時不需要我解釋了,單字節bytes對象會緩存起來,不是單字節則不會緩存。
bytearray對象
除了bytes對象之外,Python中還有一個bytearray對象,它和bytes對象類似,只不過bytes對象是不可變的,而bytearray對象是可變的。所以就不單獨分析了,這里簡單提一嘴。
# 傳入一個整型組成的列表創建bytearray對象
s = bytearray([99, 100, 101])
print(s) # bytearray(b'cde')
# 傳入一個bytes對象創建bytearray對象
s = bytearray(b"abc")
print(s)
# 傳入一個字符串,同時指定encoding編碼創建bytearray對象
s = bytearray("古明地覺", encoding="utf-8")
print(s) # bytearray(b'\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe8\xa7\x89')
# 我們對s進行decode會直接得到字符串
print(s.decode("utf-8")) # 古明地覺
# 注意:bytearray對象是可以變的
# 如果是中文,為了防止出現亂碼,所以一次要改變3個字節
s[-3:] = "戀".encode("utf-8")
print(s) # bytearray(b'\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe6\x81\x8b')
print(s.decode("utf-8")) # 古明地戀
# 我們同樣可以根據索引、切片獲取
s = bytearray(b"abc")
# 獲取單個元素也會得到整型,這一點和bytes對象是一樣的
print(s[0], s[1], s[2]) # 97 98 99
# 通過切片得到bytearray
print(s[:2]) # bytearray(b'ab')
# 對多個bytearray對象進行join, 會得到一個bytes對象
print(b"--".join([bytearray(b"abc"), bytearray(b"def")])) # b'abc--def'
因此把bytearray對象想象成可變的bytes對象即可,它的使用和bytes對象非常類似,一些操作的行為也是一樣的,所以就不單獨分析了,下一篇將會分析Python中的字符串。
小結
這次我們分析了bytes對象的底層實現,我們說:
bytes對象是一個變長、不可變對象,內部的值是通過一個C的字符數組來維護的;
bytes也是序列型操作,它支持的操作在bytes_as_sequence中;
Python內部維護字符緩沖池來優化單字節bytes對象的創建和銷毀操作;
緩沖池是一種常用的以空間換時間的優化技術;