詳解字符串在Python內(nèi)部是如何省內(nèi)存的
起步
Python3 起,str 就采用了 Unicode 編碼(注意這里并不是 utf8 編碼,盡管 .py 文件默認(rèn)編碼是 utf8 )。 每個(gè)標(biāo)準(zhǔn) Unicode 字符占用 4 個(gè)字節(jié)。這對(duì)于內(nèi)存來說,無疑是一種浪費(fèi)。
Unicode 是表示了一種字符集,而為了傳輸方便,衍生出里如 utf8 , utf16 等編碼方案來節(jié)省存儲(chǔ)空間。Python內(nèi)部存儲(chǔ)字符串也采用了類似的形式。
三種內(nèi)部表示Unicode字符串
為了減少內(nèi)存的消耗,Python使用了三種不同單位長(zhǎng)度來表示字符串:
- 每個(gè)字符 1 個(gè)字節(jié)(Latin-1)
- 每個(gè)字符 2 個(gè)字節(jié)(UCS-2)
- 每個(gè)字符 4 個(gè)字節(jié)(UCS-4)
源碼中定義字符串結(jié)構(gòu)體:
# 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 碼范圍內(nèi),那么就可以用占用 1 個(gè)字節(jié)的 Latin-1 編碼進(jìn)行存儲(chǔ)。而如果字符串中存在了需要占用兩個(gè)字節(jié)(比如中文字符),那么整個(gè)字符串就將采用占用 2 個(gè)字節(jié) UCS-2 編碼進(jìn)行存儲(chǔ)。
這點(diǎn)可以通過 sys.getsizeof 函數(shù)外部窺探來驗(yàn)證這個(gè)結(jié)論:
如圖,存儲(chǔ) 'zh' 所需的存儲(chǔ)空間比 'z' 多 1 個(gè)字節(jié), h 在這里占了 1 個(gè)字節(jié);
存儲(chǔ) 'z中' 所需的存儲(chǔ)空間比 '中' 多了 2 個(gè)字節(jié),z 在這里占了 2 個(gè)字節(jié)。
大多數(shù)的自然語言采用 2 字節(jié)的編碼就夠了。但如果有一個(gè) 1G 的 ascii 文本加載到內(nèi)存后,在文本中插入了一個(gè) emoji 表情,那么字符串所需的空間將擴(kuò)大到 4 倍,是不是很驚喜。
為什么內(nèi)部不采用 utf8 進(jìn)行編碼
最受歡迎的 Unicode 編碼方案,Python內(nèi)部卻不使用它,為什么?
這里就得說下 utf8 編碼帶來的缺點(diǎn)。這種編碼方案每個(gè)字符的占用字節(jié)長(zhǎng)度是變化的,這就導(dǎo)致了無法按所以隨機(jī)訪問單個(gè)字符,例如 string[n] (使用utf8編碼)則需要先統(tǒng)計(jì)前n個(gè)字符占用的字節(jié)長(zhǎng)度。所以由 O(1) 變成了 O(n) ,這更無法讓人接受。
因此Python內(nèi)部采用了定長(zhǎng)的方式存儲(chǔ)字符串。
字符串駐留機(jī)制
另一個(gè)節(jié)省內(nèi)存的方式就是將一些短小的字符串做成池,當(dāng)程序要?jiǎng)?chuàng)建字符串對(duì)象前檢查池中是否有滿足的字符串。在內(nèi)部中,僅包含下劃線(_)、字母 和 數(shù)字 的長(zhǎng)度不高過 20 的字符串才能駐留。駐留是在代碼編譯期間進(jìn)行的,代碼中的如下會(huì)進(jìn)行駐留檢查:
- 空字符串 '' 及所有;
- 變量名;
- 參數(shù)名;
- 字符串常量(代碼中定義的所有字符串);
- 字典鍵;
- 屬性名稱;
駐留機(jī)制節(jié)省大量的重復(fù)字符串內(nèi)存。在內(nèi)部,字符串駐留池由一個(gè)全局的 dict 維護(hù),該字段將字符串用作鍵:
void PyUnicode_InternInPlace(PyObject **p) { PyObject *s = *p; PyObject *t; if (s == NULL || !PyUnicode_Check(s)) return; // 對(duì)PyUnicodeObjec進(jìn)行類型和狀態(tài)檢查 if (!PyUnicode_CheckExact(s)) return; if (PyUnicode_CHECK_INTERNED(s)) return; // 創(chuàng)建intern機(jī)制的dict if (interned == NULL) { interned = PyDict_New(); if (interned == NULL) { PyErr_Clear(); /* Don't leave an exception */ return; } } // 對(duì)象是否存在于inter中 t = PyDict_SetDefault(interned, s, s); // 存在, 調(diào)整引用計(jì)數(shù) 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 機(jī)制中的字符串不被回收,設(shè)置字典時(shí) PyDict_SetDefault(interned, s, s); 將字符串作為鍵同時(shí)也作為值進(jìn)行設(shè)置,這樣對(duì)于字符串對(duì)象的引用計(jì)數(shù)就會(huì)進(jìn)行兩次 +1 操作,這樣存于字典中的對(duì)象在程序結(jié)束前永遠(yuǎn)不會(huì)為 0,這也是 y_REFCNT(s) -= 2; 將計(jì)數(shù)減 2 的原因。
從函數(shù)參數(shù)中可以看到其實(shí)字符串對(duì)象還是被創(chuàng)建了,內(nèi)部其實(shí)始終會(huì)為字符串創(chuàng)建對(duì)象,但經(jīng)過 inter 機(jī)制檢查后,臨時(shí)創(chuàng)建的字符串會(huì)因引用計(jì)數(shù)為 0 而被銷毀,臨時(shí)變量在內(nèi)存中曇花一現(xiàn)然后迅速消失。
字符串緩沖池
除了字符串駐留池,Python 還會(huì)保存所有 ascii 碼內(nèi)的單個(gè)字符:
static PyObject *unicode_latin1[256] = {NULL};
如果字符串其實(shí)是一個(gè)字符,那么優(yōu)先從緩沖池中獲?。?/p>
[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]); } ... }
然后再經(jīng)過 intern 機(jī)制后被保存到 intern 池中,這樣駐留池中和緩沖池中,兩者都是指向同一個(gè)字符串對(duì)象了。
嚴(yán)格來說,這個(gè)單字符緩沖池并不是省內(nèi)存的方案,因?yàn)閺闹腥〕龅膶?duì)象幾乎都會(huì)保存到緩沖池中,這個(gè)方案是為了減少字符串對(duì)象的創(chuàng)建。
總結(jié)
本文介紹了兩種是節(jié)省內(nèi)存的方案。一個(gè)字符串的每個(gè)字符在占用空間大小是相同的,取決于字符串中的最大字符。
短字符串會(huì)放到一個(gè)全局的字典中,該字典中的字符串成了單例模式,從而節(jié)省內(nèi)存。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Python 字符串處理特殊空格\xc2\xa0\t\n Non-breaking space
- 基于python3實(shí)現(xiàn)倒敘字符串
- Python日期格式和字符串格式相互轉(zhuǎn)換的方法
- 通過python檢測(cè)字符串的字母
- python如何把字符串類型list轉(zhuǎn)換成list
- python字符串,元組,列表,字典互轉(zhuǎn)代碼實(shí)例詳解
- python字符串下標(biāo)與切片及使用方法
- python 實(shí)現(xiàn)字符串下標(biāo)的輸出功能
- python判斷變量是否為int、字符串、列表、元組、字典的方法詳解
- Python基礎(chǔ)之字符串操作常用函數(shù)集合
- python字符串替換re.sub()實(shí)例解析
- Python如何訪問字符串中的值
- python3 字符串知識(shí)點(diǎn)學(xué)習(xí)筆記
- Python輸出指定字符串的方法
- python求一個(gè)字符串的所有排列的實(shí)現(xiàn)方法
- 代碼總結(jié)Python2 和 Python3 字符串的區(qū)別
- Python字符串中刪除特定字符的方法
- Python拼接字符串的7種方式詳解
相關(guān)文章
基于Python和openCV實(shí)現(xiàn)圖像的全景拼接詳細(xì)步驟
這篇文章主要介紹了基于Python和openCV實(shí)現(xiàn)圖像的全景拼接,本文分步驟通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-10-10一篇文章帶你學(xué)習(xí)Python3的高階函數(shù)
這篇文章主要為大家詳細(xì)介紹了Python3的高階函數(shù),主要介紹什么是高階函數(shù),高階函數(shù)的用法以及幾個(gè)常見的內(nèi)置的高階函數(shù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01

python中*args與**kwarsg及閉包和裝飾器的用法

python3中利用filter函數(shù)輸出小于某個(gè)數(shù)的所有回文數(shù)實(shí)例