Python中的字元串對象(《Python源碼剖析》筆記三)

這是我的關於《Python源碼剖析》一書的筆記的第三篇。Learn Python by Analyzing Python Source Code · GitBook

Python中的字元串對象

在Python3中,str類型就是Python2的unicode類型,之前的str類型轉化成了一個新的bytes類型。我們可以分析bytes類型的實現,也就是《Python源碼剖析》中的內容,但鑒於我們對str類型的常用程度,且我們對它較淺的理解,所以我們來剖析一下這個相較而言複雜得多的類型。

在之前的分析中,Python2中的整數對象是定長對象,而字元串對象則是變長對象。同時字元串對象又是一個不可變對象,創建之後就無法再改變它的值。

Unicode的四種形式

在Python3中,一個unicode字元串有四種形式:

  1. compact ascii
  2. compact
  3. legacy string, not ready
  4. legacy string ,ready

compact的意思是,假如一個字元串對象是compact的模式,它將只使用一個內存塊來存儲內容,也就是說,在內存中字元是緊緊跟在結構體後面的。對於non-compact的對象來說,也就是PyUnicodeObject,Python使用一個內存塊來保存PyUnicodeObject結構體,另一個內存塊來保存字元。

對於ASCII-only的字元串,Python使用PyUnicode_New來創建,並將其保存在PyASCIIObject結構體中。只要它是通過UTF-8來解碼的,utf-8字元串就是數據本身,也就是說兩者等價。

legacy string 是通過PyUnicodeObject來保存的。

我們先看源碼,然後再敘述其他內容。

typedef struct {n PyObject_HEADn Py_ssize_t length; /* Number of code points in the string */n Py_hash_t hash; /* Hash value; -1 if not set */n struct {n unsigned int interned:2;n unsigned int kind:3;n unsigned int compact:1;n unsigned int ascii:1;n unsigned int ready:1; n unsigned int :24;n } state;n wchar_t *wstr; /* wchar_t representation (null-terminated) */n} PyASCIIObject;nntypedef struct {n PyASCIIObject _base;n Py_ssize_t utf8_length; /* Number of bytes in utf8, excluding then * terminating 0. */n char *utf8; /* UTF-8 representation (null-terminated) */n Py_ssize_t wstr_length; /* Number of code points in wstr, possiblen * surrogates count as two code points. */n} PyCompactUnicodeObject;nntypedef struct {n PyCompactUnicodeObject _base;n union {n void *any;n Py_UCS1 *latin1;n Py_UCS2 *ucs2;n Py_UCS4 *ucs4;n } data; /* Canonical, smallest-form Unicode buffer */n} PyUnicodeObject;n

可以看出,整個字元串對象機制以PyASCIIObject為基礎,我們就先來看這個對象。length中保存了字元串中code points的數量。hash中則保存了字元串的hash值,因為一個字元串對象是不可變對象,它的hash值永遠不會改變,因此Python將其緩存在hash變數中,防止重複計算帶來的性能損失。state結構體中保存了關於這個對象的一些信息,它們和我們之前介紹的字元串的四種形式有關。wstr變數則是字元串對象真正的值所在。

state結構體中的變數都是什麼意思?為了節省篇幅,我將注釋刪除了,我們來一一解釋。interned變數的值和字元串對象的intern機制有關,它可以有三個值:SSTATE_NOT_INTERNED (0),SSTATE_INTERNED_MORTAL (1),SSTATE_INTERNED_IMMORTAL (2)。分別表示不intern,intern但可刪除,永久intern。具體的機制我們後面會說。kind主要是表示字元串以幾位元組的形式保存。compact我們已經解釋,ascii也很好理解。ready則是用來說明對象的布局是否被初始化。如果是1,就說明要麼這個對象是緊湊的(compact),要麼它的數據指針已經被填滿了。

我們前面提到,一個ASCII字元串使用PyUnicode_New來創建,並保存在PyASCIIObject結構體中。同樣使用PyUnicode_New創建的字元串對象,如果是非ASCII字元串,則保存在PyCompactUnicodeObject結構體中。一個PyUnicodeObject通過PyUnicode_FromUnicode(NULL, len)創建,真正的字元串數據一開始保存在wstr block中,然後使用_PyUnicode_Ready被複制到了data block中。

我們再來看一下PyUnicode_Type:

PyTypeObject PyUnicode_Type = {n PyVarObject_HEAD_INIT(&PyType_Type, 0)n "str", /* tp_name */n sizeof(PyUnicodeObject), /* tp_size */n ……n unicode_repr, /* tp_repr */n &unicode_as_number, /* tp_as_number */n &unicode_as_sequence, /* tp_as_sequence */n &unicode_as_mapping, /* tp_as_mapping */n (hashfunc) unicode_hash, /* tp_hash*/n ……n};n

可以看出,Python3中的str的確就是之前的unicode。

創建字元串對象

PyObject *PyUnicode_FromUnicode(const Py_UNICODE *u, Py_ssize_t size)n{n PyObject *unicode;n Py_UCS4 maxchar = 0;n Py_ssize_t num_surrogates;nn if (u == NULL)n return (PyObject*)_PyUnicode_New(size);nn /* If the Unicode data is known at construction time, we can applyn some optimizations which share commonly used objects. */nn /* Optimization for empty strings */n if (size == 0)n _Py_RETURN_UNICODE_EMPTY();nn /* Single character Unicode objects in the Latin-1 range aren shared when using this constructor */n if (size == 1 && (Py_UCS4)*u < 256)n return get_latin1_char((unsigned char)*u);nn /* If not empty and not single character, copy the Unicode datan into the new object */n if (find_maxchar_surrogates(u, u + size,n &maxchar, &num_surrogates) == -1)n return NULL;nn unicode = PyUnicode_New(size - num_surrogates, maxchar);n if (!unicode)n return NULL;nn switch (PyUnicode_KIND(unicode)) {n case PyUnicode_1BYTE_KIND:n _PyUnicode_CONVERT_BYTES(Py_UNICODE, unsigned char,n u, u + size, PyUnicode_1BYTE_DATA(unicode));n break;n case PyUnicode_2BYTE_KIND:n#if Py_UNICODE_SIZE == 2n memcpy(PyUnicode_2BYTE_DATA(unicode), u, size * 2);n#elsen _PyUnicode_CONVERT_BYTES(Py_UNICODE, Py_UCS2,n u, u + size, PyUnicode_2BYTE_DATA(unicode));n#endifn break;n case PyUnicode_4BYTE_KIND:n#if SIZEOF_WCHAR_T == 2n /* This is the only case which has to process surrogates, thusn a simple copy loop is not enough and we need a function. */n unicode_convert_wchar_to_ucs4(u, u + size, unicode);n#elsen assert(num_surrogates == 0);n memcpy(PyUnicode_4BYTE_DATA(unicode), u, size * 4);n#endifn break;n default:n assert(0 && "Impossible state");n }nn return unicode_result(unicode);n}nPyObject *PyUnicode_New(Py_ssize_t size, Py_UCS4 maxchar)n{n PyObject *obj;n PyCompactUnicodeObject *unicode;n void *data;n enum PyUnicode_Kind kind;n int is_sharing, is_ascii;n Py_ssize_t char_size;n Py_ssize_t struct_size;nn /* Optimization for empty strings */n if (size == 0 && unicode_empty != NULL) {n Py_INCREF(unicode_empty);n return unicode_empty;n }nn is_ascii = 0;n is_sharing = 0;n struct_size = sizeof(PyCompactUnicodeObject);n if (maxchar < 128) {n kind = PyUnicode_1BYTE_KIND;n char_size = 1;n is_ascii = 1;n struct_size = sizeof(PyASCIIObject);n }n else if (maxchar < 256) {n kind = PyUnicode_1BYTE_KIND;n char_size = 1;n }n else if (maxchar < 65536) {n kind = PyUnicode_2BYTE_KIND;n char_size = 2;n if (sizeof(wchar_t) == 2)n is_sharing = 1;n }n else {n if (maxchar > MAX_UNICODE) {n PyErr_SetString(PyExc_SystemError,n "invalid maximum character passed to PyUnicode_New");n return NULL;n }n kind = PyUnicode_4BYTE_KIND;n char_size = 4;n if (sizeof(wchar_t) == 4)n is_sharing = 1;n }nn /* Ensure we wont overflow the size. */n if (size < 0) {n PyErr_SetString(PyExc_SystemError,n "Negative size passed to PyUnicode_New");n return NULL;n }n if (size > ((PY_SSIZE_T_MAX - struct_size) / char_size - 1))n return PyErr_NoMemory();nn /* Duplicated allocation code from _PyObject_New() instead of a call ton * PyObject_New() so we are able to allocate space for the object andn * its data buffer.n */n obj = (PyObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size);n if (obj == NULL)n return PyErr_NoMemory();n obj = PyObject_INIT(obj, &PyUnicode_Type);n if (obj == NULL)n return NULL;nn unicode = (PyCompactUnicodeObject *)obj;n if (is_ascii)n data = ((PyASCIIObject*)obj) + 1;n elsen data = unicode + 1;n _PyUnicode_LENGTH(unicode) = size;n _PyUnicode_HASH(unicode) = -1;n _PyUnicode_STATE(unicode).interned = 0;n _PyUnicode_STATE(unicode).kind = kind;n _PyUnicode_STATE(unicode).compact = 1;n _PyUnicode_STATE(unicode).ready = 1;n _PyUnicode_STATE(unicode).ascii = is_ascii;n if (is_ascii) {n ((char*)data)[size] = 0;n _PyUnicode_WSTR(unicode) = NULL;n }n else if (kind == PyUnicode_1BYTE_KIND) {n ((char*)data)[size] = 0;n _PyUnicode_WSTR(unicode) = NULL;n _PyUnicode_WSTR_LENGTH(unicode) = 0;n unicode->utf8 = NULL;n unicode->utf8_length = 0;n }n else {n unicode->utf8 = NULL;n unicode->utf8_length = 0;n if (kind == PyUnicode_2BYTE_KIND)n ((Py_UCS2*)data)[size] = 0;n else /* kind == PyUnicode_4BYTE_KIND */n ((Py_UCS4*)data)[size] = 0;n if (is_sharing) {n _PyUnicode_WSTR_LENGTH(unicode) = size;n _PyUnicode_WSTR(unicode) = (wchar_t *)data;n }n else {n _PyUnicode_WSTR_LENGTH(unicode) = 0;n _PyUnicode_WSTR(unicode) = NULL;n }n }n#ifdef Py_DEBUGn unicode_fill_invalid((PyObject*)unicode, 0);n#endifn assert(_PyUnicode_CheckConsistency((PyObject*)unicode, 0));n return obj;n}n

先來分析PyUnicode_FromUnicode的流程。如果傳入的u是個空指針,調用_PyUnicode_New(size)直接返回一個指定大小但值為空的PyUnicodeObject對象。如果size==0,調用_Py_RETURN_UNICODE_EMPTY()直接返回。如果是在Latin-1範圍內的單字元字元串,直接返回該字元對應的PyUnicodeObject,這和我們在上一章說的小整數對象池類似,這裡也有一個字元緩衝池。如果兩者都不是,則創建一個新的對象並將數據複製到這個對象中。

PyUnicode_New的流程很好理解,傳入對象的大小和maxchar,根據這兩個參數來決定返回的是PyASCIIObject,PyCompactUnicodeObject還是PyUnicodeObject。

Intern機制

我們之前提到了intern機制,它指的就是在創建一個新的字元串對象時,如果已經有了和它的值相同的字元串對象,那麼就直接返回那個對象的引用,而不返回新創建的字元串對象。Python在那裡尋找呢?事實上,python維護著一個鍵值對類型的結構interned,鍵就是字元串的值。但這個intern機制並非對於所有的字元串對象都適用,簡單來說對於那些符合python標識符命名原則的字元串,也就是只包括字母數字下劃線的字元串,python會對它們使用intern機制。在標準庫中,有一個函數可以讓我們對一個字元串強制實行這個機制——sys.intern(),下面是這個函數的文檔:

Enter string in the table of 「interned」 strings and return the interned string – which is string itself or a copy. Interning strings is useful to gain a little performance on dictionary lookup – if the keys in a dictionary are interned, and the lookup key is interned, the key comparisons (after hashing) can be done by a pointer compare instead of a string compare. Normally, the names used in Python programs are automatically interned, and the dictionaries used to hold module, class or instance attributes have interned keys.

Interned strings are not immortal; you must keep a reference to the return value of intern() around to benefit from it.

具體機制見下面代碼:

PyObject *PyUnicode_InternFromString(const char *cp)n{n PyObject *s = PyUnicode_FromString(cp);n if (s == NULL)n return NULL;n PyUnicode_InternInPlace(&s);n return s;n}nvoid PyUnicode_InternInPlace(PyObject **p)n{n PyObject *s = *p;n PyObject *t;n#ifdef Py_DEBUGn assert(s != NULL);n assert(_PyUnicode_CHECK(s));n#elsen if (s == NULL || !PyUnicode_Check(s))n return;n#endifn /* If its a subclass, we dont really know what puttingn it in the interned dict might do. */n if (!PyUnicode_CheckExact(s))n return;n if (PyUnicode_CHECK_INTERNED(s))n return;n if (interned == NULL) {n interned = PyDict_New();n if (interned == NULL) {n PyErr_Clear(); /* Dont leave an exception */n return;n }n }n Py_ALLOW_RECURSIONn t = PyDict_SetDefault(interned, s, s);n Py_END_ALLOW_RECURSIONn if (t == NULL) {n PyErr_Clear();n return;n }n if (t != s) {n Py_INCREF(t);n Py_SETREF(*p, t);n return;n }n /* The two references in interned are not counted by refcnt.n The deallocator will take care of this */n Py_REFCNT(s) -= 2;n _PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;n}n

當Python調用PyUnicode_InternFromString時,會返回一個interned的對象,具體過程由PyUnicode_InternInPlace來實現。

事實上,即使Python會對一個字元串進行intern操作,它也會先創建出一個PyUnicodeObject對象,之後再檢查是否有值和其相同的對象。如果有的話,就將interned中保存的對象返回,之前新創建出來的,因為引用計數變為零,被回收了。

被intern機制處理後的對象分為兩類:mortal和immortal,前者會被回收,後者則不會被回收,與Python虛擬機共存亡。

PyUnicodeObject有關的效率問題

在《Python源碼剖析》原書中提到使用+來連接字元串是一個極其低效的操作,因為每次連接都會創建一個新的字元串對象,推薦使用字元串的join方法來連接字元串。在Python3.6下,經過我的測試,使用+來連接字元串已經和使用join的耗時相差不大。當然這只是我在個別環境下的測試,真正的答案我還不知道。

小結

在Python3中,str底層實現使用unicode,這很好的解決了Python2中複雜麻煩的非ASCII字元串的種種問題。同時在底層,Python對於ASCII和非ASCII字元串區別對待,加上utf-8兼容ASCII字元,兼顧了性能和簡單程度。在Python中,不可變對象往往都有類似intern機制的東西,這使得Python減少了不必要的內存消耗,但是在真正的實現中,Python也是取平衡點。因為,一味使用intern機制,有可能會造成額外的計算和查找,這就和優化性能的目的背道而馳了。


推薦閱讀:

[18] Python元組
第八章 Python爬蟲實戰(1):爬取Drupal論壇帖子列表
寫了一個scrapy爬蟲,為什麼運行提示找不到Douban.items這個模塊??
25 歲才開始學習編程靠譜嗎?40 歲都不晚!

TAG:Python | C编程语言 |