Unicode是什么
計(jì)算機(jī)存儲(chǔ)的基本單位是 八位字節(jié) ,由 8 個(gè)比特位組成,簡(jiǎn)稱(chēng) 字節(jié) 。由于英文只由 26 個(gè)字母加若干符號(hào)組成,因此英文字符可以直接用 字節(jié) 來(lái)保存。其他諸如中日韓等語(yǔ)言,由于字符眾多,則不得不用多個(gè)字節(jié)來(lái)編碼。
隨著計(jì)算機(jī)技術(shù)的傳播,非拉丁文字符編碼技術(shù)蓬勃發(fā)展,但存在兩個(gè)比較大的局限性:
- 不支持多語(yǔ)言 ,例如中文的編碼方案不能表示日文;
- 沒(méi)有統(tǒng)一標(biāo)準(zhǔn) ,例如中文有 GB2312 ,GBK 、 GB18030 等多種編碼標(biāo)準(zhǔn);
由于編碼方式不統(tǒng)一,開(kāi)發(fā)人員經(jīng)常需要在不同編碼間來(lái)回轉(zhuǎn)化,錯(cuò)誤頻出。為了徹底解決這些問(wèn)題, 統(tǒng)一碼聯(lián)盟 提出了 Unicode 標(biāo)準(zhǔn)。Unicode 對(duì)世界上大部分文字系統(tǒng)進(jìn)行整理、編碼,讓計(jì)算機(jī)可以用統(tǒng)一的方式處理文本。Unicode 目前已經(jīng)收錄了超過(guò) 13 萬(wàn)個(gè)字符,天然地支持多語(yǔ)言。使用 Unicode ,即可徹底跟編碼問(wèn)題說(shuō)拜拜!
Python中的Unicode
Python 在 3 之后,str 對(duì)象內(nèi)部改用 Unicode 表示,因而被源碼稱(chēng)為 Unicode 對(duì)象。這么做好處是顯然易見(jiàn)的,程序核心邏輯統(tǒng)一用 Unicode ,只需在輸入、輸入層進(jìn)行編碼、解碼,可最大程度避免各種編碼問(wèn)題:

由于 Unicode 收錄字符已經(jīng)超過(guò) 13 萬(wàn)個(gè),每個(gè)字符至少需要 4 個(gè)字節(jié)來(lái)保存。這意味著巨大的內(nèi)存開(kāi)銷(xiāo),顯然是不可接受的。英文字符用 ASCII 表示僅需 1 個(gè)字節(jié),而用 Unicode 表示內(nèi)存開(kāi)銷(xiāo)卻增加 4 倍!
Python 作者們肯定不允許這樣的事情發(fā)生,不信我們先來(lái)觀察下( getsizeof 獲取對(duì)象內(nèi)存大小):
>>> import sys
# 英文字符還是1字節(jié)
>>> sys.getsizeof('ab') - sys.getsizeof('a')
1
# 中文字符需要2字節(jié)
>>> sys.getsizeof('中國(guó)') - sys.getsizeof('中')
2
# Emoji表情需要4字節(jié)
>>> sys.getsizeof('??') - sys.getsizeof('?')
4
- 每個(gè) ASCII 英文字符,占用 1 字節(jié);
- 每個(gè)中文字符,占用 2 字節(jié);
- Emoji 表情,占用 4 字節(jié);
由此可見(jiàn),Python 內(nèi)部對(duì) Unicode 進(jìn)行優(yōu)化:根據(jù)文本內(nèi)容,選擇底層存儲(chǔ)單元。至于這種黑科技是怎么實(shí)現(xiàn)的,我們只能到源碼中尋找答案了。與 str 對(duì)象實(shí)現(xiàn)相關(guān)源碼如下:
- Include/unicodeobject.h
- Objects/unicodectype.c
在 Include/unicodeobject.h 頭文件中,我們發(fā)現(xiàn) str 對(duì)象底層存儲(chǔ)根據(jù)文本字符 Unicode 碼位范圍分成幾類(lèi):
- PyUnicode_1BYTE_KIND ,所有字符碼位均在 U+0000 到 U+00FF 之間;
- PyUnicode_2BYTE_KIND ,所有字符碼位均在 U+0000 到 U+FFFF 之間,且至少一個(gè)大于 U+00FF;
- PyUnicode_4BYTE_KIND ,所有字符碼位均在 U+0000 到 U+10FFFF 之間,且至少一個(gè)大于 U+FFFF;
enum PyUnicode_Kind {
/* String contains only wstr byte characters. This is only possible
when the string was created with a legacy API and _PyUnicode_Ready()
has not been called yet. */
PyUnicode_WCHAR_KIND = 0,
/* Return values of the PyUnicode_KIND() macro: */
PyUnicode_1BYTE_KIND = 1,
PyUnicode_2BYTE_KIND = 2,
PyUnicode_4BYTE_KIND = 4
};
如果文本字符碼位均在 U+0000 到 U+00FF 之間,單個(gè)字符只需 1 字節(jié)來(lái)表示;而碼位在 U+0000 到 U+FFFF 之間的文本,單個(gè)字符則需要 2 字節(jié)才能表示;以此類(lèi)推。這樣一來(lái),根據(jù)文本碼位范圍,便可為字符選用盡量小的存儲(chǔ)單元,以最大限度節(jié)約內(nèi)存。
typedef uint32_t Py_UCS4;
typedef uint16_t Py_UCS2;
typedef uint8_t Py_UCS1;
文本類(lèi)型字符存儲(chǔ)單元字符存儲(chǔ)單元大小(字節(jié))PyUnicode_1BYTE_KINDPy_UCS11PyUnicode_2BYTE_KINDPy_UCS22PyUnicode_4BYTE_KINDPy_UCS44
Unicode 內(nèi)部存儲(chǔ)結(jié)構(gòu)因文本類(lèi)型而異,因此類(lèi)型 kind 必須作為 Unicode 對(duì)象公共字段保存。Python 內(nèi)部定義了若干個(gè) 標(biāo)志位 ,作為 Unicode 公共字段,kind 便是其中之一:
- interned ,是否為 interned 機(jī)制維護(hù), internel 機(jī)制在本節(jié)后半部分介紹;
- kind ,類(lèi)型,用于區(qū)分字符底層存儲(chǔ)單元大小;
- compact ,內(nèi)存分配方式,對(duì)象與文本緩沖區(qū)是否分離,本文不涉及分離模式;
- ascii ,文本是否均為純 ASCII ;
Objects/unicodectype.c 源文件中的 PyUnicode_New 函數(shù),根據(jù)文本字符數(shù) size 以及最大字符 maxchar 初始化 Unicode 對(duì)象。該函數(shù)根據(jù) maxchar 為 Unicode 對(duì)象選擇最緊湊的字符存儲(chǔ)單元以及底層結(jié)構(gòu)體:
maxchar < 128maxchar < 256maxchar < 65536maxchar < MAX_UNICODEkindPyUnicode_1
BYTE_KINDPyUnicode_1
BYTE_KINDPyUnicode_2
BYTE_KINDPyUnicode_4
BYTE_KINDascii1000字符存儲(chǔ)單元大小1124底層結(jié)構(gòu)體PyASCIIObjectPyCompact
UnicodeObjectPyCompact
UnicodeObjectPyCompact
UnicodeObject
PyASCIIObject
如果 str 對(duì)象保存的文本均為 ASCII ,即 maxchar<128maxchar<128,則底層由 PyASCIIObject 結(jié)構(gòu)存儲(chǔ):
/* ASCII-only strings created through PyUnicode_New use the PyASCIIObject
structure. state.ascii and state.compact are set, and the data
immediately follow the structure. utf8_length and wstr_length can be found
in the length field; the utf8 pointer is equal to the data pointer. */
typedef struct {
PyObject_HEAD
Py_ssize_t length; /* Number of code points in the string */
Py_hash_t hash; /* Hash value; -1 if not set */
struct {
unsigned int interned:2;
unsigned int kind:3;
unsigned int compact:1;
unsigned int ascii:1;
unsigned int ready:1;
unsigned int :24;
} state;
wchar_t *wstr; /* wchar_t representation (null-terminated) */
} PyASCIIObject;
PyASCIIObject 結(jié)構(gòu)體也是其他 Unicode 底層存儲(chǔ)結(jié)構(gòu)體的基礎(chǔ),所有字段均為 Unicode 公共字段:
- ob_refcnt ,引用計(jì)數(shù);
- ob_type ,對(duì)象類(lèi)型;
- length ,文本長(zhǎng)度;
- hash ,文本哈希值;
- state ,Unicode 對(duì)象標(biāo)志位,包括 internel 、 kind 、 ascii 、 compact 等;
- wstr ,略;

注意到,state 字段后有一個(gè) 4 字節(jié)的空洞,這是結(jié)構(gòu)體字段 內(nèi)存對(duì)齊 造成的現(xiàn)象。在 64 位機(jī)器下,指針大小為 8 字節(jié),為優(yōu)化內(nèi)存訪問(wèn)效率,wstr 必須以 8 字節(jié)對(duì)齊;而 state 字段大小只是 4 字節(jié),便留下 4 字節(jié)的空洞。PyASCIIObject 結(jié)構(gòu)體大小在 64 位機(jī)器下為 48 字節(jié),在 32 位機(jī)器下為 24 字節(jié)。
ASCII 文本則緊接著位于 PyASCIIObject 結(jié)構(gòu)體后面,以字符串對(duì)象 ‘abc’ 以及空字符串對(duì)象 ‘’ 為例:

注意到,與 bytes 對(duì)象一樣,Python 也在 ASCII 文本末尾,額外添加一個(gè) 字符,以兼容 C 字符串。
如此一來(lái),以 Unicode 表示的 ASCII 文本,額外內(nèi)存開(kāi)銷(xiāo)僅為 PyASCIIObject 結(jié)構(gòu)體加上末尾的 字節(jié)而已。PyASCIIObject 結(jié)構(gòu)體在 64 位機(jī)器下,大小為 48 字節(jié)。因此,長(zhǎng)度為 n 的純 ASCII 字符串對(duì)象,需要消耗 n+48+1,即 n+49 字節(jié)的內(nèi)存空間。
>>> sys.getsizeof('')
49
>>> sys.getsizeof('abc')
52
>>> sys.getsizeof('a' * 10000)
10049
PyCompactUnicodeObject
如果文本不全是 ASCII ,Unicode 對(duì)象底層便由 PyCompactUnicodeObject 結(jié)構(gòu)體保存:
/* Non-ASCII strings allocated through PyUnicode_New use the
PyCompactUnicodeObject structure. state.compact is set, and the data
immediately follow the structure. */
typedef struct {
PyASCIIObject _base;
Py_ssize_t utf8_length; /* Number of bytes in utf8, excluding the
* terminating . */
char *utf8; /* UTF-8 representation (null-terminated) */
Py_ssize_t wstr_length; /* Number of code points in wstr, possible
* surrogates count as two code points. */
} PyCompactUnicodeObject;
PyCompactUnicodeObject 在 PyASCIIObject 基礎(chǔ)上,增加 3 個(gè)字段:
- utf8_length ,文本 UTF8 編碼長(zhǎng)度;
- utf8 ,文本 UTF8 編碼形式,緩存以避免重復(fù)編碼運(yùn)算;
- wstr_length ,略;

由于 ASCII 本身兼容 UTF8 ,無(wú)須保存 UTF8 編碼形式,這也是 ASCII 文本底層由 PyASCIIObject 保存的原因。在 64 位機(jī)器,PyCompactUnicodeObject 結(jié)構(gòu)體大小為 72 字節(jié);在 32 位機(jī)器則是 36 字節(jié)。
PyUnicode_1BYTE_KIND
如果 128<=maxchar<256128<=maxchar<256,Unicode 對(duì)象底層便由 PyCompactUnicodeObject 結(jié)構(gòu)體保存,字符存儲(chǔ)單元為 Py_UCS1 ,大小為 1 字節(jié)。以 Python® 為例,字符 ® 碼位為 U+00AE ,滿足該條件,內(nèi)部結(jié)構(gòu)如下:

字符存儲(chǔ)單元還是 1 字節(jié),跟 ASCII 文本一樣。 因此,Python® 對(duì)象需要占用 80 字節(jié)的內(nèi)存空間72+1*7+1=72+8=8072+1∗7+1=72+8=80:
>>> sys.getsizeof('Python®')
80
PyUnicode_2BYTE_KIND
如果 256<=maxchar<65536256<=maxchar<65536,Unicode 對(duì)象底層同樣由 PyCompactUnicodeObject 結(jié)構(gòu)體保存,但字符存儲(chǔ)單元為 Py_UCS2 ,大小為 2 字節(jié)。以 AC米蘭 為例,常用漢字碼位在 U+0100 到 U+FFFF 之間,滿足該條件,內(nèi)部結(jié)構(gòu)如下:

由于現(xiàn)在字符存儲(chǔ)單元為 2 字節(jié),故而 str 對(duì)象 AC米蘭 需要占用 82 字節(jié)的內(nèi)存空間:72+2*4+2=72+10=8272+2∗4+2=72+10=82
>>> sys.getsizeof('AC米蘭')
82
我們看到,當(dāng)文本包含中文后,英文字母也只能用 2 字節(jié)的存儲(chǔ)單元來(lái)保存了。
你可能會(huì)提出疑問(wèn),為什么不采用變長(zhǎng)存儲(chǔ)單元呢?例如,字母 1 字節(jié),漢字 2 字節(jié)?這是因?yàn)椴捎米冮L(zhǎng)存儲(chǔ)單元后,就無(wú)法在 O(1) 時(shí)間內(nèi)取出文本第 n 個(gè)字符了——你只能從頭遍歷直到遇到第 n 個(gè)字符。
PyUnicode_4BYTE_KIND
如果 65536<=maxchar<42949629665536<=maxchar<429496296,便只能用 4 字節(jié)存儲(chǔ)單元 Py_UCS4 了。以 AC米蘭? 為例:

>>> sys.getsizeof('AC米蘭')
96
這樣一來(lái),給一段英文文本加上表情,內(nèi)存暴增 4 倍,也就不奇怪了:
>>> text = 'a' * 1000
>>> sys.getsizeof(text)
1049
>>> text += '?'
>>> sys.getsizeof(text)
4080
interned機(jī)制
如果 str 對(duì)象 interned 標(biāo)識(shí)位為 1 ,Python 虛擬機(jī)將為其開(kāi)啟 interned 機(jī)制。那么,什么是 interned 機(jī)制?
先考慮以下場(chǎng)景,如果程序中有大量 User 對(duì)象,有什么可優(yōu)化的地方?
>>> class User:
...
... def __init__(self, name, age):
... self.name = name
... self.age = age
...
>>>
>>> user = User(name='tom', age=20)
>>> user.__dict__
{'name': 'tom', 'age': 20}
由于對(duì)象的屬性由 dict 保存,這意味著每個(gè) User 對(duì)象都需要保存 str 對(duì)象 name 。換句話講,1 億個(gè) User 對(duì)象需要重復(fù)保存 1 億個(gè)同樣的 str 對(duì)象,這將浪費(fèi)多少內(nèi)存!
由于 str 是不可變對(duì)象,因此 Python 內(nèi)部將有潛在重復(fù)可能的字符串都做成 單例模式 ,這就是 interned 機(jī)制。Python 具體做法是在內(nèi)部維護(hù)一個(gè)全局 dict 對(duì)象,所有開(kāi)啟 interned 機(jī)制 str 對(duì)象均保存在這里;后續(xù)需要用到相關(guān)對(duì)象的地方,則優(yōu)先到全局 dict 中取,避免重復(fù)創(chuàng)建。
舉個(gè)例子,雖然 str 對(duì)象 ‘abc’ 由不同的運(yùn)算產(chǎn)生,但背后卻是同一個(gè)對(duì)象:
>>> a = 'abc'
>>> b = 'ab' + 'c'
>>> id(a), id(b), a is b
(4424345224, 4424345224, True)