起步 Python3 起,str 就採用了 Unicode 編碼(註意這裡並不是 utf8 編碼,儘管 .py 文件預設編碼是 utf8 )。 每個標準 Unicode 字元占用 4 個位元組。這對於記憶體來說,無疑是一種浪費。 Unicode 是表示了一種字元集,而為了傳輸方便,衍生出里如 utf8 ...
起步
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教程項目,還可以跟老司機交流討教!
本文的文字及圖片來源於網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理。