起步
Python3 起,str 就采用了 Unicode 編碼(注意這里并不是 utf8 編碼,盡管 .py 檔案默認編碼是 utf8 ), 每個標準 Unicode 字符占用 4 個位元組,這對于記憶體來說,無疑是一種浪費,
Unicode 是表示了一種字符集,而為了傳輸方便,衍生出里如 utf8 , utf16 等編碼方案來節省存盤空間,Python內部存盤字串也采用了類似的形式,
另外還要注意:不管你是為了Python就業還是興趣愛好,記住:專案開發經驗永遠是核心,如果你沒有2020最新python入門到高級實戰視頻教程,可以去小編的Python交流.裙 :七衣衣九七七巴而五(數字的諧音)轉換下可以找到了,里面很多新python教程專案,還可以跟老司機交流討教!
三種內部表示Unicode字串
為了減少記憶體的消耗,Python使用了三種不同單位長度來表示字串:
- 每個字符 1 個位元組(Latin-1)
- 每個字符 2 個位元組(UCS-2)
- 每個字符 4 個位元組(UCS-4)
原始碼中定義字串結構體:
# Include/unicodeobject.h
typedef uint32_t Py_UCS4;
typedef uint16_t Py_UCS2;
typedef uint8_t Py_UCS1;
# Include/cpython/unicodeobject.h
typedef struct {
PyCompactUnicodeObject _base;
union {
void *any;
Py_UCS1 *latin1;
Py_UCS2 *ucs2;
Py_UCS4 *ucs4;
} data; /* Canonical, smallest-form Unicode buffer */
} PyUnicodeObject;
復制代碼
如果字串中所有字符都在 ascii 碼范圍內,那么就可以用占用 1 個位元組的 Latin-1 編碼進行存盤,而如果字串中存在了需要占用兩個位元組(比如中文字符),那么整個字串就將采用占用 2 個位元組 UCS-2 編碼進行存盤,
這點可以通過 sys.getsizeof 函式外部窺探來驗證這個結論:
如圖,存盤 'zh' 所需的存盤空間比 'z' 多 1 個位元組, h 在這里占了 1 個位元組;
存盤 'z中' 所需的存盤空間比 '中' 多了 2 個位元組,z 在這里占了 2 個位元組,
大多數的自然語言采用 2 位元組的編碼就夠了,但如果有一個 1G 的 ascii 文本加載到記憶體后,在文本中插入了一個 emoji 表情,那么字串所需的空間將擴大到 4 倍,是不是很驚喜,
為什么內部不采用 utf8 進行編碼
最受歡迎的 Unicode 編碼方案,Python內部卻不使用它,為什么?
這里就得說下 utf8 編碼帶來的缺點,這種編碼方案每個字符的占用位元組長度是變化的,這就導致了無法按所以隨機訪問單個字符,例如 string[n] (使用utf8編碼)則需要先統計前n個字符占用的位元組長度,所以由 O(1) 變成了 O(n) ,這更無法讓人接受,
因此Python內部采用了定長的方式存盤字串,
字串駐留機制
另一個節省記憶體的方式就是將一些短小的字串做成池,當程式要創建字串物件前檢查池中是否有滿足的字串,在內部中,僅包含下劃線(_)、字母 和 數字 的長度不高過 20 的字串才能駐留,駐留是在代碼編譯期間進行的,代碼中的如下會進行駐留檢查:
- 空字串
''及所有; - 變數名;
- 引數名;
- 字串常量(代碼中定義的所有字串);
- 字典鍵;
- 屬性名稱;
駐留機制節省大量的重復字串記憶體,在內部,字串駐留池由一個全域的 dict 維護,該欄位將字串用作鍵:
void PyUnicode_InternInPlace(PyObject **p)
{
PyObject *s = *p;
PyObject *t;
if (s == NULL || !PyUnicode_Check(s))
return;
// 對PyUnicodeObjec進行型別和狀態檢查
if (!PyUnicode_CheckExact(s))
return;
if (PyUnicode_CHECK_INTERNED(s))
return;
// 創建intern機制的dict
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
// 物件是否存在于inter中
t = PyDict_SetDefault(interned, s, s);
// 存在, 調整參考計數
if (t != s) {
Py_INCREF(t);
Py_SETREF(*p, t);
return;
}
/* The two references in interned are not counted by refcnt.
The deallocator will take care of this */
Py_REFCNT(s) -= 2;
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}
復制代碼
變數 interned 就是全域存放字串池的字典的變數名 interned = PyDict_New(),為了讓 intern 機制中的字串不被回收,設定字典時 PyDict_SetDefault(interned, s, s); 將字串作為鍵同時也作為值進行設定,這樣對于字串物件的參考計數就會進行兩次 +1 操作,這樣存于字典中的物件在程式結束前永遠不會為 0,這也是 y_REFCNT(s) -= 2; 將計數減 2 的原因,
從函式引數中可以看到其實字串物件還是被創建了,內部其實始侄訓為字串創建物件,但經過 inter 機制檢查后,臨時創建的字串會因參考計數為 0 而被銷毀,臨時變數在記憶體中曇花一現然后迅速消失,
字串緩沖池
除了字串駐留池,Python 還會保存所有 ascii 碼內的單個字符:
static PyObject *unicode_latin1[256] = {NULL};
復制代碼
如果字串其實是一個字符,那么優先從緩沖池中獲取:
[unicodeobjec.c]
PyObject * PyUnicode_DecodeUTF8Stateful(const char *s,
Py_ssize_t size,
const char *errors,
Py_ssize_t *consumed)
{
...
/* ASCII is equivalent to the first 128 ordinals in Unicode. */
if (size == 1 && (unsigned char)s[0] < 128) {
return get_latin1_char((unsigned char)s[0]);
}
...
}
復制代碼
然后再經過 intern 機制后被保存到 intern 池中,這樣駐留池中和緩沖池中,兩者都是指向同一個字串物件了,
嚴格來說,這個單字符緩沖池并不是省記憶體的方案,因為從中取出的物件幾乎都會保存到緩沖池中,這個方案是為了減少字串物件的創建,
總結
本文介紹了兩種是節省記憶體的方案,一個字串的每個字符在占用空間大小是相同的,取決于字串中的最大字符,
短字串會放到一個全域的字典中,該字典中的字串成了單例模式,從而節省記憶體,
最后注意:不管你是為了Python就業還是興趣愛好,記住:專案開發經驗永遠是核心,如果你沒有2020最新python入門到高級實戰視頻教程,可以去小編的Python交流.裙 :七衣衣九七七巴而五(數字的諧音)轉換下可以找到了,里面很多新python教程專案,還可以跟老司機交流討教!
本文的文字及圖片來源于網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,著作權歸原作者所有,如有問題請及時聯系我們以作處理,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/155876.html
標籤:Python
上一篇:recursion:遞回演示
下一篇:P1013 進制位
