1. 程式人生 > >Python字串物件實現原理

Python字串物件實現原理

在Python世界中將物件分為兩種:一種是定長物件,比如整數,整數物件定義的時候就能確定它所佔用的記憶體空間大小,另一種是變長物件,在物件定義時並不知道是多少,比如:str,list, set, dict等。

>>> import sys
>>> sys.getsizeof(1000)
28
>>> sys.getsizeof(2000)
28
>>> sys.getsizeof("python")
55
>>> sys.getsizeof("java")
53

如上,整數物件所佔用的記憶體都是28位元組,和具體的值沒關係,而同樣都是字串物件,不同字串物件所佔用的記憶體是不一樣的,這就是變長物件,對於變長物件,在物件定義時是不知道物件所佔用的記憶體空間是多少的。

字串物件在Python內部用PyStringObject表示,PyStringObject和PyIntObject一樣都屬於不可變物件,物件一旦建立就不能改變其值。(注意:變長物件不可變物件是兩個不同的概念)。PythonStringObject的定義:

[stringobject.h]
typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;
    int ob_sstate;
    char ob_sval[1];
} PyStringObject;

不難看出Python的字串物件內部就是由一個字元陣列維護的,在整數的實現原理

一文中提到PyObject_HEAD,對於PyObject_VAR_HEAD就是在PyObject_HEAD基礎上多出一個ob_size屬性:

[object.h]
#define PyObject_VAR_HEAD       
    PyObject_HEAD           
    int ob_size; /* Number of items in variable part */

typedef struct {
    PyObject_VAR_HEAD
} PyVarObject;
  • ob_size儲存了變長物件中元素的長度,比如PyStringObject物件"Python"的ob_size
    為6。
  • ob_sval是一個初始大小為1的字元陣列,且ob_sval[0] = '\0',但實際上建立一個PyStringObject時ob_sval指向的是一段長為ob_size+1個位元組的記憶體。
  • ob_shash是字串物件的雜湊值,初始值為-1,在第一次計算出字串的雜湊值後,會把該值快取下來,賦值給ob_shash
  • ob_sstate用於標記該字串物件是否進過intern機制處理(後文會介紹)。

PyStringObject物件建立過程

[stringobject.c]
PyObject * PyString_FromString(const char *str)
{
    register size_t size;
    register PyStringObject *op;

    assert(str != NULL);
    size = strlen(str);
    // [1]
    if (size > PY_SSIZE_T_MAX - PyStringObject_SIZE) {
        PyErr_SetString(PyExc_OverflowError,
            "string is too long for a Python string");
        return NULL;
    }
    // [2]
    if (size == 0 && (op = nullstring) != NULL) {
#ifdef COUNT_ALLOCS
        null_strings++;
#endif
        Py_INCREF(op);
        return (PyObject *)op;
    }
    // [3]
    if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
#ifdef COUNT_ALLOCS
        one_strings++;
#endif
        Py_INCREF(op);
        return (PyObject *)op;
    }

    // [4]
    /* Inline PyObject_NewVar */
    op = (PyStringObject *)PyObject_MALLOC(PyStringObject_SIZE + size);
    if (op == NULL)
        return PyErr_NoMemory();
    PyObject_INIT_VAR(op, &PyString_Type, size);
    op->ob_shash = -1;
    op->ob_sstate = SSTATE_NOT_INTERNED;
    Py_MEMCPY(op->ob_sval, str, size+1);
    /* share short strings */
    if (size == 0) {
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        nullstring = op;
        Py_INCREF(op);
    } else if (size == 1) {
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        characters[*str & UCHAR_MAX] = op;
        Py_INCREF(op);
    }
    return (PyObject *) op;
}
  1. 如果字串的長度超出了Python所能接受的最大長度(32位平臺是2G),則返回Null。
  2. 如果是空字串,那麼返回特殊的PyStringObject,即nullstring。
  3. 如果字串的長度為1,那麼返回特殊PyStringObject,即onestring。
  4. 其他情況下就是分配記憶體,初始化PyStringObject,把引數str的字元陣列拷貝到PyStringObject中的ob_sval指向的記憶體空間。

字串的intern機制

PyStringObject的ob_sstate屬性用於標記字串物件是否經過intern機制處理,intern處理後的字串,比如"Python",在直譯器執行過程中始終只有唯一的一個字串"Python"對應的PyStringObject物件。

>>> a = "python"
>>> b = "python"
>>> a is b
True

如上所示,建立a時,系統首先會建立一個新的PyStringObject物件出來,然後經過intern機制處理(PyString_InternInPlace),接著查詢經過intern機制處理的PyStringObject物件,如果發現有該字串對應的PyStringObject存在,則直接返回該物件,否則把剛剛建立的PyStringObject加入到intern機制中。由於a和b字串字面值是一樣的,因此a和b都指向同一個PyStringObject("python")物件。那麼intern內部又是一個什麼樣的機制呢?

[stringobject.c]
static PyObject *interned;

void PyString_InternInPlace(PyObject **p)
{
    register PyStringObject *s = (PyStringObject *)(*p);
    PyObject *t;
    if (s == NULL || !PyString_Check(s))
        Py_FatalError("PyString_InternInPlace: strings only please!");
    /* If it's a string subclass, we don't really know what putting
       it in the interned dict might do. */
    // [1]
    if (!PyString_CheckExact(s))
        return;
    // [2]
    if (PyString_CHECK_INTERNED(s))
        return;
    // [3]
    if (interned == NULL) {
        interned = PyDict_New();
        if (interned == NULL) {
            PyErr_Clear(); /* Don't leave an exception */
            return;
        }
    }
    t = PyDict_GetItem(interned, (PyObject *)s);
    if (t) {
        Py_INCREF(t);
        Py_DECREF(*p);
        *p = t;
        return;
    }

    if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {
        PyErr_Clear();
        return;
    }
    /* The two references in interned are not counted by refcnt.
       The string deallocator will take care of this */
    Py_REFCNT(s) -= 2;
    PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}
  1. 先型別檢查,intern機制只處理字串
  2. 如果該PyStringObject物件已經進行過intern機制處理,則直接返回
  3. interned其實一個字典物件,當它為null時,初始化一個字典物件,否則,看該字典中是否存在一個key為(PyObject *)s的value,如果存在,那麼就把該物件的引用計數加1,臨時建立的那個物件的引用計數減1。否則,把(PyObject *)s同時作為key和value新增到interned字典中,與此同時它的引用計數減2,這兩個引用計數減2是因為被interned字典所引用,但這兩個引用不作為垃圾回收的判斷依據,否則,字串物件永遠都不會被垃圾回收器收集了。

intern

上述程式碼中,給b賦值為"python"後,系統中建立了幾個PyStringObject物件呢?答案是:2,在建立b的時候,一定會有一個臨時的PyStringObject作為字典的key在interned中查詢是否存在一個PyStringObject物件的值為"python"。

字串的緩衝池

字串除了有intern機制快取字串之外,字串還有一種專門的短字串緩衝池characters。用於快取字串長度為1的PyStringObject物件。

    static PyStringObject *characters[UCHAR_MAX + 1];   //UCHAR_MAX = 255

建立長度為1的字串時流程:

...
 else if (size == 1) {
    PyObject *t = (PyObject *)op;
    PyString_InternInPlace(&t);
    op = (PyStringObject *)t;
    characters[*str & UCHAR_MAX] = op;
    Py_INCREF(op);
  1. 首先建立一個PyStringObject物件。
  2. 進行intern操作
  3. 將PyStringObject快取到characters中
  4. 引用計數增1

characters

總結:
1. 字串用PyStringObject表示 2. 字串屬於變長物件 3. 字串屬於不可變物件 4. 字串用intern機制提高python的效率 5. 字串有專門的緩衝池儲存長度為1的字串物件


關注公眾號「Python之禪」(id:vttalk)獲取最新文章 python之禪