成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

解密 Python 元組的實現(xiàn)原理

開發(fā) 前端
元組如果可哈希,那么元組存儲的元素必須都是可哈希的。只要有一個元素不可哈希,那么元組就會不可哈希。比如元組里面存儲了一個列表,由于列表不可哈希,導致存儲了列表的元組也會變得不可哈希。

楔子

本篇文章來聊一聊元組,元組可以簡單理解為不支持元素添加、修改、刪除等操作的列表,也就是在列表的基礎(chǔ)上移除了增刪改操作。

所以從功能上來講,元組只是列表的子集,那元組存在的意義是什么呢?首先元組可以作為字典的 key 以及集合的元素,因為字典和集合使用的數(shù)據(jù)結(jié)構(gòu)是哈希表,它存儲的元素一定是可哈希的,關(guān)于字典和集合我們后續(xù)章節(jié)會說。

而列表可以動態(tài)改變,所以列表不支持哈希。因此當我們希望字典的 key 是一個序列時,顯然元組再適合不過了。比如要根據(jù)年齡和身高統(tǒng)計人數(shù),那么就可以將年齡和身高組成元組作為字典的 key,人數(shù)作為字典的 value。所以元組可哈希,能夠作為哈希表的 key,是元組存在的意義之一。當然元組還有其它作用,我們稍后再說。

元組如果可哈希,那么元組存儲的元素必須都是可哈希的。只要有一個元素不可哈希,那么元組就會不可哈希。比如元組里面存儲了一個列表,由于列表不可哈希,導致存儲了列表的元組也會變得不可哈希。

元組的底層結(jié)構(gòu)

根據(jù)我們使用元組的經(jīng)驗,可以得出元組是一個變長對象,但同時又是一個不可變對象。

// Include/cpython/tupleobject.h
typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];
} PyTupleObject;

以上是元組在底層對應的結(jié)構(gòu)體,包含引用計數(shù)、類型、ob_size、指針數(shù)組。然后數(shù)組聲明的長度雖然是 1,但我們可以當成 n 來用。

然后我們再通過結(jié)構(gòu)體的定義,來對比一下它和列表的區(qū)別。首先元組沒有 allocated、也就是容量的概念,這是因為它是不可變的,不支持 resize 操作。

另一個區(qū)別就是元組對應的指針數(shù)組是定義在結(jié)構(gòu)體里面的,可以直接對數(shù)組進行操作。而列表對應的指針數(shù)組是定義在結(jié)構(gòu)體外面的,兩者通過二級指針進行關(guān)聯(lián),也就是通過二級指針來間接操作指針數(shù)組。

至于為什么要這么定義,我們在最開始介紹對象模型的時候也說得很詳細了。可變對象的具體元素不會保存在結(jié)構(gòu)體內(nèi)部,而是會維護一個指針,指針指向的內(nèi)存區(qū)域負責存儲元素。當發(fā)生擴容時,只需改變指針指向即可,從而方便內(nèi)存管理。

基于結(jié)構(gòu)體的定義,我們也能分析出元組所占的內(nèi)存大小,顯然它等于 24 + 8 * 元組長度。

圖片圖片

結(jié)果沒有問題。

元組是怎么創(chuàng)建的?

元組支持的操作我們就不看了,因為它只支持查詢操作,并且和列表是高度相似的。這里我們直接來看元組的創(chuàng)建過程。

正如列表一樣,解釋器為創(chuàng)建 PyTupleObject 也提供了類似的初始化方法,即 PyTuple_New。

// Objects/tupleobject.c
PyObject *
PyTuple_New(Py_ssize_t size)
{
    // 參數(shù) size 表示元組的長度
    PyTupleObject *op;
    // 如果 size 為 0,返回空數(shù)組
    if (size == 0) {
        return tuple_get_empty();
    }
    // 調(diào)用 tuple_alloc 為元組申請內(nèi)存
    op = tuple_alloc(size);
    // 如果返回的指針為 NULL,表示申請失敗
    if (op == NULL) {
        return NULL;
    }
    // 將指針數(shù)組中所有元素設置為 NULL
    for (Py_ssize_t i = 0; i < size; i++) {
        op->ob_item[i] = NULL;
    }
    // 讓 GC 進行跟蹤
    _PyObject_GC_TRACK(op);
    // 轉(zhuǎn)成泛型指針之后返回
    return (PyObject *) op;
}

相信這種代碼邏輯現(xiàn)在對你來說已經(jīng)沒有任何難度了,然后我們看到里面調(diào)用了 tuple_alloc,該函數(shù)實際負責元組的內(nèi)存申請過程,來看看它的內(nèi)部實現(xiàn)。

// Objects/tupleobject.c
static PyTupleObject *
tuple_alloc(Py_ssize_t size)
{
    // size 必須大于等于 0
    if (size < 0) {
        PyErr_BadInternalCall();
        return NULL;
    }
    // 優(yōu)先從緩存池中獲取,所以元組也有緩存池
    // 關(guān)于元組的緩存池稍后再聊
    PyTupleObject *op = maybe_freelist_pop(size);
    // 如果 op 為 NULL,說明緩存池無可用元素
    if (op == NULL) {
        // size * sizeof(PyObject *) + sizeof(PyTupleObject) 便是元組大小
        // 該值不能超過 PY_SSIZE_T_MAX,否則報錯
        if ((size_t)size > ((size_t)PY_SSIZE_T_MAX - (sizeof(PyTupleObject) -
                    sizeof(PyObject *))) / sizeof(PyObject *)) {
            return (PyTupleObject *)PyErr_NoMemory();
        }
        // 為 PyTupleObject 和長度為 size 的指針數(shù)組申請內(nèi)存
        // 然后將它的類型設置為 &PyTuple_Type,將 ob_size 設置為 size
        op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
        if (op == NULL)
            return NULL;
    }
    // 返回 op
    return op;
}

tuple_alloc 負責申請內(nèi)存,當內(nèi)存申請完畢之后,PyTuple_New 再將它的 ob_item 里面的元素設置為 NULL,即初始化。

以上就是元組創(chuàng)建的過程,但里面隱藏了很多的細節(jié)沒有說,下面我們來介紹元組的緩存池,然后將細節(jié)一一揭開。

元組的緩存池

元組的緩存池也是通過數(shù)組來實現(xiàn)的。

// Include/internal/pycore_tuple.h
#define PyTuple_MAXSAVESIZE 20
#define PyTuple_NFREELISTS PyTuple_MAXSAVESIZE
#define PyTuple_MAXFREELIST 2000

struct _Py_tuple_state {
    PyTupleObject *free_list[PyTuple_NFREELISTS];
    int numfree[PyTuple_NFREELISTS];
};

里面出現(xiàn)了三個宏:

  • PyTuple_MAXSAVESIZE:緩存池的大小,默認為 20;
  • PyTuple_NFREELISTS:緩存池的每個元素都對應一條鏈表(稍后解釋),該宏表示鏈表的數(shù)量,因此它和 PyTuple_MAXSAVESIZE 是等價的,也表示緩存池的大小;
  • PyTuple_MAXFREELIST:每個鏈表最多容納多少個節(jié)點(稍后解釋);

從定義中可以看到,元組的緩存池大小是 20,而我們之前介紹的列表的緩存池大小是 80。但這里的 20 和 80 還稍微有些不同,80 指的是列表緩存池的大小,除此之外沒有別的含義。而 20 除了表示元組緩存池的大小之外,它還表示只有當元組的長度不超過 20,回收時才會被放入緩存池。

當元組的長度為 n 時(其中 n <= 20),那么在回收的時候該元組就會放在緩存池中索引為 n - 1 的位置。假設回收的元組長度為 6,那么就會放在緩存池索引為 5 的位置。

但是問題來了,如果要回收兩個長度為 6 的元組該怎么辦?很簡單,像鏈表一樣串起來就好了。所以 free_list 里面雖然存儲的是 PyTupleObject *,但每個 (PyTupleObject *)->ob_item[0] 都存儲了下一個 PyTupleObject *。

因此你可以認為 free_list 存儲了 20 條鏈表的頭結(jié)點的指針,每條鏈表上面掛著具有相同 ob_size 的 PyTupleObject。比如 free_list[n - 1] 便指向了長度為 n 的 PyTupleObject 組成的鏈表的頭結(jié)點。

至于每條鏈表的節(jié)點個數(shù)由 numfree 維護,并且最大不能超過 PyTuple_MAXFREELIST,默認是 2000。

圖片圖片

這里再來重新捋一下,元組的緩存池是一個數(shù)組,并且索引為 n - 1 的位置回收的是元素個數(shù)(ob_size)為 n 的元組,并且 n 不超過 20。但這樣的話,具有相同長度的元組不就只能緩存一個了嗎?比如我們有很多個長度為 2 的元組都要緩存怎么辦呢?

顯然將它們以鏈表的形式串起來即可,正如圖中顯示的那樣。至于長度為 n 的元組究竟緩存了多少個,則由 numfree[n-1] 負責維護。假設 free_list[2] 這條鏈表上掛了 1000 個 PyTupleObject,那么 numfree[2] 就等于 1000,即長度為 3 的元組被緩存了 1000 個。

當再回收一個長度為 3 的元組時,那么會讓該元組的 ob_item[0] 等于 free_list[2],然后 free_list[2] 等于該元組、numfree[2]++。所以這里的每一條鏈表和浮點數(shù)緩存池是類似的,也是采用的頭插法。

我們看一下放入緩存池的具體過程,顯然這一步發(fā)生在元組銷毀的時候。

// Objects/tupleobject.c
static void
tupledealloc(PyTupleObject *op)
{   
    // 緩存池存放的是長度為 1 ~ 20 的元組
    // 如果是空元組,那么它是單例的永恒對象,會永遠存在
    // 因此這里會直接返回,不做任何額外操作
    if (Py_SIZE(op) == 0) {
        if (op == &_Py_SINGLETON(tuple_empty)) {
            return;
    }
    // 讓 GC 不再跟蹤
    PyObject_GC_UnTrack(op);
    // 延遲釋放,和列表是類似的
    Py_TRASHCAN_BEGIN(op, tupledealloc)
    // 獲取銷毀的元組的長度
    Py_ssize_t i = Py_SIZE(op);
    // 減少內(nèi)部元素指向?qū)ο蟮囊糜嫈?shù),因為元組不再持有對它們的引用
    while (--i >= 0) {
        Py_XDECREF(op->ob_item[i]);
    }
    // 嘗試放入緩存池
    if (!maybe_freelist_push(op)) {
        // 如果元組長度大于 20,或者緩存池已滿
        // 那么釋放內(nèi)存
        Py_TYPE(op)->tp_free((PyObject *)op);
    }

    Py_TRASHCAN_END
}

/* 在前面的章節(jié)中我們說了,在 3.12 之前
   對象的緩存池是以靜態(tài)全局變量的形式定義在文件中,以 3.8 的元組緩存池為例
   static PyTupleObject *free_list[PyTuple_MAXSAVESIZE];
   static int numfree[PyTuple_MAXSAVESIZE];
   但在 3.12 的時候則被封裝在了一個結(jié)構(gòu)體(_Py_tuple_state)中
   在解釋器啟動之后會靜態(tài)初始化好,并綁定在進程狀態(tài)對象上面 */
// 所以這里的 STATE 便負責獲取元組的緩存池,即 _Py_tuple_state 結(jié)構(gòu)體實例
// 它里面包含了 numfree 和 free_list
#define STATE (interp->tuple)

// 將元組放入緩存池
static inline int
maybe_freelist_push(PyTupleObject *op)
{
#if PyTuple_NFREELISTS > 0
    // 獲取進程狀態(tài)對象,interp->tuple 便是元組的緩存池
    PyInterpreterState *interp = _PyInterpreterState_GET();
    // 如果元組長度為 0,不做處理
    if (Py_SIZE(op) == 0) {
        return0;
    }
    // free_list 里面的每個元素都指向了一個鏈表的頭結(jié)點
    // 每條鏈表存放的元組都具有相同的長度,如果元組長度為 n
    // 那么它會放在 free_list[n - 1] 對應的鏈表中
    Py_ssize_t index = Py_SIZE(op) - 1;
    // index 必須小于 20,即元組長度不超過 20
    // STATE.numfree[index] 必須小于 2000,即每條鏈表最多緩存 2000 個元組
    if (index < PyTuple_NFREELISTS
        && STATE.numfree[index] < PyTuple_MAXFREELIST
        && Py_IS_TYPE(op, &PyTuple_Type))
    {
        // ob_item[0] 充當了鏈表的 next 指針
        // 這里讓 op->ob_item[0] 等于 free_list[index]
        // 然后讓 free_list[index] 等于 op
        // 這樣元組就緩存起來了,并成為鏈表新的頭結(jié)點,即 free_list[index]
        op->ob_item[0] = (PyObject *) STATE.free_list[index];
        STATE.free_list[index] = op;
        // 然后維護一下鏈表的節(jié)點個數(shù)
        STATE.numfree[index]++;
        OBJECT_STAT_INC(to_freelist);
        return1;
    }
#endif
    return0;
}

tupledealloc 函數(shù)在銷毀元組時,會調(diào)用 maybe_freelist_push 函數(shù),嘗試放入緩存池中。那么同理,tuple_alloc 函數(shù)在創(chuàng)建元組時,也會調(diào)用 maybe_freelist_pop 函數(shù),嘗試從緩存池中獲取。

// Objects/tupleobject.c
static inline PyTupleObject *
maybe_freelist_pop(Py_ssize_t size)
{
#if PyTuple_NFREELISTS > 0
    // 獲取進程狀態(tài)對象
    PyInterpreterState *interp = _PyInterpreterState_GET();
    // size 不可能為 0
    // 如果 size 為 0,那么在 PyTuple_New 中會直接返回空元組
    if (size == 0) {
        return NULL;
    }
    assert(size > 0);
    // 緩存池中每個元素都指向一個鏈表的頭結(jié)點
    // PyTuple_NFREELISTS 表示鏈表的個數(shù),PyTuple_MAXSAVESIZE 表示緩存池的大小
    // 所以這兩個宏是等價的,默認都是 20
    // 只有元組的長度不超過 20 的時候,才會被緩存
    if (size <= PyTuple_MAXSAVESIZE) {
        Py_ssize_t index = size - 1;
        // 獲取鏈表的頭節(jié)點
        PyTupleObject *op = STATE.free_list[index];
        if (op != NULL) {
            // 獲取之后,它的下一個節(jié)點要成為新的頭結(jié)點
            STATE.free_list[index] = (PyTupleObject *) op->ob_item[0];
            // 鏈表節(jié)點個數(shù)減 1
            STATE.numfree[index]--;
            // 增加引用計數(shù)之后返回
            _Py_NewReference((PyObject *)op);
            OBJECT_STAT_INC(from_freelist);
            return op;
        }
    }
#endif
    return NULL;
}

到此,相信你已經(jīng)明白元組的緩存池到底是怎么一回事了,說白了就是有 20 條鏈表,分別負責緩存長度為 1 ~ 20 的元組,它們的頭結(jié)點指針會保存在緩存池中。

然后每條鏈表的長度不超過 2000,也就是具有相同長度的元組最多回收 2000 個。至于鏈表的 next 指針,則由元組的 ob_item[0] 來充當,通過 ob_item[0] 來獲取下一個節(jié)點。

圖片圖片

我們看到打印的地址是一樣的,因為第一次創(chuàng)建的元組被重復利用了。

那么問題來了,為什么元組緩存池可以緩存的元組個數(shù)會這么多,每個鏈表緩存 2000 個,有 20 條鏈表,總共可以緩存 40000 個。這么做的原因就是,元組的使用頻率遠比我們想象的廣泛,主要是它大量使用在我們看不到的地方。比如多元賦值:

a, b, c, d = 1, 2, 3, 4

在編譯時,上面的 1, 2, 3, 4 實際上是作為元組被加載的,整個賦值相當于元組的解包。再比如函數(shù)、方法的返回值,如果是多返回值,本質(zhì)上也是包裝成一個元組之后再返回。

所以元組緩存池能緩存的對象個數(shù),要遠大于其它對象的緩存池。可以想象一個大型項目,里面的函數(shù)、方法不計其數(shù),只要是多返回值,就會涉及到元組的創(chuàng)建,因此每種長度的元組緩存 2000 個是很合理的。當然如果長度超過 20,就不會緩存了,這種元組的使用頻率沒有那么高。

然后再回顧一下元組的回收過程,會發(fā)現(xiàn)它和列表有一個很大的不同。列表在被回收時,它的指針數(shù)組會被釋放;但元組不同,它在被回收時,底層的指針數(shù)組會保留,并且還巧妙地通過索引來記錄了回收的元組的大小規(guī)格。

元組的這項技術(shù)也被稱為靜態(tài)資源緩存,因為元組在執(zhí)行析構(gòu)函數(shù)時,不僅對象本身沒有被回收,連底層的指針數(shù)組也被緩存起來了。那么當再次分配時,速度就會快一些。

from timeit import timeit

t1 = timeit(stmt="x1 = [1, 2, 3, 4, 5]", number=1000000)
t2 = timeit(stmt="x2 = (1, 2, 3, 4, 5)", number=1000000)

print(round(t1, 2))  # 0.05
print(round(t2, 2))  # 0.01

可以看到耗時,元組只是列表的五分之一。這便是元組的另一個優(yōu)勢,可以將資源緩存起來。而緩存的原因還是如上面所說,因為涉及大量的創(chuàng)建和銷毀,所以這一切都是為了加快內(nèi)存分配。

由于對象都在堆區(qū),為了效率,Python 不得不大量使用緩存的技術(shù)。

最后再回答一個遺漏的部分,就是當元組長度為 0 的情況。我們說如果元組長度為 1 到  20,在回收時會被緩存起來,各自對應一條鏈表,鏈表上面能容納的元素個數(shù)不超過 2000。

但如果長度為 0 呢?

圖片圖片

從源碼中可以看到,如果長度為 0,會調(diào)用 tuple_get_empty。

// Objects/tupleobject.c
static inline PyObject *
tuple_get_empty(void)
{
    return Py_NewRef(&_Py_SINGLETON(tuple_empty));
}

// Include/internal/pycore_global_objects.h
struct _Py_static_objects {
    struct {
        // ...
        PyTupleObject tuple_empty;
        // ...
    } singletons;
};

// Include/internal/pycore_runtime_init.h
#define _PyRuntimeState_INIT(runtime) \
    { \
        .static_objects = { \
            .singletons = { \
             /* ... */ \
                .tuple_empty = { \
                    .ob_base = _PyVarObject_HEAD_INIT(&PyTuple_Type, 0) \
                }, \
                /* ... */ \
            }, \
        }, \
    }

所以長度為 0 的元組是一個靜態(tài)單例對象,解釋器啟動之后就已經(jīng)初始化好了,并且它也是一個永恒對象。

圖片圖片

永恒對象的引用計數(shù)為 2 ** 32 - 1,并且不會發(fā)生變化。

小結(jié)

以上就是元組相關(guān)的內(nèi)容,因為有了列表相關(guān)的經(jīng)驗,再來看元組就會快很多。當然啦,元組的一些操作我們沒有說,因為和對應的列表操作是類似的。

最后再補充一下,列表是有 __init__ 方法的,而元組沒有。

圖片圖片

元組的 __init__ 直接繼承 object.__init__。

對啦,再分享一個有趣的事情,就是元組的緩存池之前是有 Bug 的,我碰巧發(fā)現(xiàn)并修復了。具體細節(jié)可以閱讀這篇文章:我?guī)?CPython 修復了一個 bug。

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關(guān)推薦

2024-09-05 10:49:42

2023-10-20 08:18:17

Python數(shù)據(jù)類型

2015-09-15 15:41:09

監(jiān)控寶Docker

2021-09-10 07:41:06

Python拷貝Python基礎(chǔ)

2023-12-05 13:46:09

解密協(xié)程線程隊列

2022-05-25 08:31:31

ArthasInstrument

2022-11-18 18:36:24

2023-11-22 08:35:34

存儲引擎bitcask

2022-01-26 07:25:09

PythonRSA加解密

2020-11-26 15:10:20

Python代碼函數(shù)

2024-05-31 08:38:35

Python浮點數(shù)屬性

2023-11-23 08:31:51

競爭鎖共享字段

2017-04-06 12:20:16

2025-02-13 09:26:43

Python元組集合

2017-05-16 15:33:42

Python網(wǎng)絡爬蟲核心技術(shù)框架

2024-10-30 08:00:00

Python列表元組

2024-03-26 06:53:41

Python元組轉(zhuǎn)換JSON對象

2017-12-06 16:28:48

Synchronize實現(xiàn)原理

2014-06-06 09:01:07

DHCP

2024-12-23 15:05:29

點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 在线播放第一页 | 久久国产精品色av免费观看 | 午夜综合 | 日日干天天操 | 国产黄色大片 | 亚洲精品一区二三区不卡 | 国产成人精品一区二区三区网站观看 | 国产综合第一页 | 日韩免费av | 久在草 | 欧美国产精品 | 97影院在线午夜 | 国产中文区二幕区2012 | 久久国产精品一区二区三区 | 日韩精品 电影一区 亚洲 | 一区二区三区精品在线 | 羞羞视频网站免费看 | 中文字幕av一区二区三区 | 日韩和的一区二区 | 日韩成人在线播放 | 日本精品视频一区二区 | 成人精品一区二区 | 精品视频在线观看 | 日日爽| 黄色av一区| 九色 在线| a黄毛片 | 日本一区二区影视 | 91视频.com | 国产黄色大片在线免费观看 | 免费在线成人网 | 视频1区| 午夜免费视频 | 亚洲一区不卡 | 亚洲欧美激情精品一区二区 | jⅰzz亚洲| 男人天堂视频在线观看 | 国产精品99久久久久久久久久久久 | 在线国产视频 | 日韩在线一区二区三区 | 久久久精品一区 |