字符串在Python内部是如何省内存的
作者:@weapon
轉自知乎:https://zhuanlan.zhihu.com/p/104844376
0. 前言
Python3 起,str 就采用了 Unicode 編碼(注意這里并不是 utf8 編碼,盡管 .py 文件默認編碼是 utf8 )。每個標準 Unicode 字符占用 4 個字節。這對于內存來說,無疑是一種浪費。
Unicode 是表示了一種字符集,而為了傳輸方便,衍生出里如 utf8 , utf16 等編碼方案來節省存儲空間。Python內部存儲字符串也采用了類似的形式。
1. 三種內部表示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 倍,是不是很驚喜。
2. 為什么內部不采用 utf8 進行編碼
最受歡迎的 Unicode 編碼方案,Python內部卻不使用它,為什么?
這里就得說下 utf8 編碼帶來的缺點。這種編碼方案每個字符的占用字節長度是變化的,這就導致了無法按索引隨機訪問單個字符,例如 string[n] (使用utf8編碼)則需要先統計前n個字符占用的字節長度。索引由 O(1) 變成了 O(n) ,這更無法讓人接受。
因此Python內部采用了定長的方式存儲字符串。
3. 字符串駐留機制
另一個節省內存的方式就是將一些短小的字符串做成池,當程序要創建字符串對象前檢查池中是否有滿足的字符串。在內部中,僅包含下劃線(_)、字母 和 數字 的長度不高過 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機制的dictif?(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 而被銷毀,臨時變量在內存中曇花一現然后迅速消失。
4. 字符串緩沖池
除了字符串駐留池,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 池中,這樣駐留池中和緩沖池中,兩者都是指向同一個字符串對象了。
嚴格來說,這個單字符緩沖池并不是省內存的方案,因為從中取出的對象幾乎都會保存到緩沖池中,這個方案是為了減少字符串對象的創建。
5. 總結一下
本文介紹了兩種是節省內存的方案。一個字符串的每個字符在占用空間大小是相同的,取決于字符串中的最大字符。
短字符串會放到一個全局的字典中,該字典中的字符串成了單例模式,從而節省內存。
來和小伙伴們一起向上生長呀!掃描下方二維碼,添加小詹微信,可領取千元大禮包并申請加入 Python 學習交流群,群內僅供學術交流,日常互動,如果是想發推文、廣告、砍價小程序的敬請繞道!一定記得備注「交流學習」,我會盡快通過好友申請哦!????長按識別,添加微信(添加人數較多,請耐心等待)????長按識別,關注小詹(掃碼回復 1024 領取程序員大禮包)總結
以上是生活随笔為你收集整理的字符串在Python内部是如何省内存的的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 推荐一些能能提高生产力的 Python
- 下一篇: websocket python爬虫_p