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

由于 Unicode 收錄字符已經超過 13 萬個,每個字符至少需要 4 個字節來保存。這意味著巨大的內存開銷,顯然是不可接受的。英文字符用 ASCII 表示僅需 1 個字節,而用 Unicode 表示內存開銷卻增加 4 倍!
Python 作者們肯定不允許這樣的事情發生,不信我們先來觀察下( getsizeof 獲取對象內存大小):
>>> import sys
# 英文字符還是1字節
>>> sys.getsizeof('ab') - sys.getsizeof('a')
1
# 中文字符需要2字節
>>> sys.getsizeof('中國') - sys.getsizeof('中')
2
# Emoji表情需要4字節
>>> sys.getsizeof('??') - sys.getsizeof('?')
4
- 每個 ASCII 英文字符,占用 1 字節;
- 每個中文字符,占用 2 字節;
- Emoji 表情,占用 4 字節;
由此可見,Python 內部對 Unicode 進行優化:根據文本內容,選擇底層存儲單元。至于這種黑科技是怎么實現的,我們只能到源碼中尋找答案了。與 str 對象實現相關源碼如下:
- Include/unicodeobject.h
- Objects/unicodectype.c
在 Include/unicodeobject.h 頭文件中,我們發現 str 對象底層存儲根據文本字符 Unicode 碼位范圍分成幾類:
- PyUnicode_1BYTE_KIND ,所有字符碼位均在 U+0000 到 U+00FF 之間;
- PyUnicode_2BYTE_KIND ,所有字符碼位均在 U+0000 到 U+FFFF 之間,且至少一個大于 U+00FF;
- PyUnicode_4BYTE_KIND ,所有字符碼位均在 U+0000 到 U+10FFFF 之間,且至少一個大于 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 之間,單個字符只需 1 字節來表示;而碼位在 U+0000 到 U+FFFF 之間的文本,單個字符則需要 2 字節才能表示;以此類推。這樣一來,根據文本碼位范圍,便可為字符選用盡量小的存儲單元,以最大限度節約內存。
typedef uint32_t Py_UCS4;
typedef uint16_t Py_UCS2;
typedef uint8_t Py_UCS1;
文本類型字符存儲單元字符存儲單元大?。ㄗ止潱?/strong>PyUnicode_1BYTE_KINDPy_UCS11PyUnicode_2BYTE_KINDPy_UCS22PyUnicode_4BYTE_KINDPy_UCS44
Unicode 內部存儲結構因文本類型而異,因此類型 kind 必須作為 Unicode 對象公共字段保存。Python 內部定義了若干個 標志位 ,作為 Unicode 公共字段,kind 便是其中之一:
- interned ,是否為 interned 機制維護, internel 機制在本節后半部分介紹;
- kind ,類型,用于區分字符底層存儲單元大?。?/li>
- compact ,內存分配方式,對象與文本緩沖區是否分離,本文不涉及分離模式;
- ascii ,文本是否均為純 ASCII ;
Objects/unicodectype.c 源文件中的 PyUnicode_New 函數,根據文本字符數 size 以及最大字符 maxchar 初始化 Unicode 對象。該函數根據 maxchar 為 Unicode 對象選擇最緊湊的字符存儲單元以及底層結構體:
maxchar < 128maxchar < 256maxchar < 65536maxchar < MAX_UNICODEkindPyUnicode_1
BYTE_KINDPyUnicode_1
BYTE_KINDPyUnicode_2
BYTE_KINDPyUnicode_4
BYTE_KINDascii1000字符存儲單元大小1124底層結構體PyASCIIObjectPyCompact
UnicodeObjectPyCompact
UnicodeObjectPyCompact
UnicodeObject
PyASCIIObject
如果 str 對象保存的文本均為 ASCII ,即 maxchar<128maxchar<128,則底層由 PyASCIIObject 結構存儲:
/* 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 結構體也是其他 Unicode 底層存儲結構體的基礎,所有字段均為 Unicode 公共字段:
- ob_refcnt ,引用計數;
- ob_type ,對象類型;
- length ,文本長度;
- hash ,文本哈希值;
- state ,Unicode 對象標志位,包括 internel 、 kind 、 ascii 、 compact 等;
- wstr ,略;

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

注意到,與 bytes 對象一樣,Python 也在 ASCII 文本末尾,額外添加一個 字符,以兼容 C 字符串。
如此一來,以 Unicode 表示的 ASCII 文本,額外內存開銷僅為 PyASCIIObject 結構體加上末尾的 字節而已。PyASCIIObject 結構體在 64 位機器下,大小為 48 字節。因此,長度為 n 的純 ASCII 字符串對象,需要消耗 n+48+1,即 n+49 字節的內存空間。
>>> sys.getsizeof('')
49
>>> sys.getsizeof('abc')
52
>>> sys.getsizeof('a' * 10000)
10049
PyCompactUnicodeObject
如果文本不全是 ASCII ,Unicode 對象底層便由 PyCompactUnicodeObject 結構體保存:
/* 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 基礎上,增加 3 個字段:
- utf8_length ,文本 UTF8 編碼長度;
- utf8 ,文本 UTF8 編碼形式,緩存以避免重復編碼運算;
- wstr_length ,略;

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

字符存儲單元還是 1 字節,跟 ASCII 文本一樣。 因此,Python® 對象需要占用 80 字節的內存空間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 對象底層同樣由 PyCompactUnicodeObject 結構體保存,但字符存儲單元為 Py_UCS2 ,大小為 2 字節。以 AC米蘭 為例,常用漢字碼位在 U+0100 到 U+FFFF 之間,滿足該條件,內部結構如下:

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

>>> sys.getsizeof('AC米蘭')
96
這樣一來,給一段英文文本加上表情,內存暴增 4 倍,也就不奇怪了:
>>> text = 'a' * 1000
>>> sys.getsizeof(text)
1049
>>> text += '?'
>>> sys.getsizeof(text)
4080
interned機制
如果 str 對象 interned 標識位為 1 ,Python 虛擬機將為其開啟 interned 機制。那么,什么是 interned 機制?
先考慮以下場景,如果程序中有大量 User 對象,有什么可優化的地方?
>>> 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}
由于對象的屬性由 dict 保存,這意味著每個 User 對象都需要保存 str 對象 name 。換句話講,1 億個 User 對象需要重復保存 1 億個同樣的 str 對象,這將浪費多少內存!
由于 str 是不可變對象,因此 Python 內部將有潛在重復可能的字符串都做成 單例模式 ,這就是 interned 機制。Python 具體做法是在內部維護一個全局 dict 對象,所有開啟 interned 機制 str 對象均保存在這里;后續需要用到相關對象的地方,則優先到全局 dict 中取,避免重復創建。
舉個例子,雖然 str 對象 ‘abc’ 由不同的運算產生,但背后卻是同一個對象:
>>> a = 'abc'
>>> b = 'ab' + 'c'
>>> id(a), id(b), a is b
(4424345224, 4424345224, True)