Python3 起,str
就採用了 Unicode
編碼(注意這裏並非 utf8
編碼,儘管 .py
文件默認編碼是 utf8
)。 每一個標準 Unicode
字符佔用 4 個字節。這對於內存來講,無疑是一種浪費。python
Unicode
是表示了一種字符集,而爲了傳輸方便,衍生出裏如 utf8
, utf16
等編碼方案來節省存儲空間。Python內部存儲字符串也採用了相似的形式。bash
爲了減小內存的消耗,Python使用了三種不一樣單位長度來表示字符串:函數
源碼中定義字符串結構體:ui
# 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
編碼進行存儲。this
這點能夠經過 sys.getsizeof
函數外部窺探來驗證這個結論:編碼
如圖,存儲 'zh'
所需的存儲空間比 'z'
多 1 個字節, h
在這裏佔了 1 個字節;spa
存儲 'z中'
所需的存儲空間比 '中'
多了 2 個字節,z
在這裏佔了 2 個字節。code
大多數的天然語言採用 2 字節的編碼就夠了。但若是有一個 1G 的 ascii 文本加載到內存後,在文本中插入了一個 emoji 表情,那麼字符串所需的空間將擴大到 4 倍,是否是很驚喜。orm
最受歡迎的 Unicode 編碼方案,Python內部卻不使用它,爲何?cdn
這裏就得說下 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 池中,這樣駐留池中和緩衝池中,二者都是指向同一個字符串對象了。
嚴格來講,這個單字符緩衝池並非省內存的方案,由於從中取出的對象幾乎都會保存到緩衝池中,這個方案是爲了減小字符串對象的建立。
本文介紹了兩種是節省內存的方案。一個字符串的每一個字符在佔用空間大小是相同的,取決於字符串中的最大字符。
短字符串會放到一個全局的字典中,該字典中的字符串成了單例模式,從而節省內存。