深入理解Python虛擬機(jī)中元組(tuple)的實(shí)現(xiàn)原理及源碼
元組的結(jié)構(gòu)
在這一小節(jié)當(dāng)中主要介紹在 python 當(dāng)中元組的數(shù)據(jù)結(jié)構(gòu):
typedef struct { PyObject_VAR_HEAD PyObject *ob_item[1]; /* ob_item contains space for 'ob_size' elements. * Items must normally not be NULL, except during construction when * the tuple is not yet visible outside the function that builds it. */ } PyTupleObject; #define PyObject_VAR_HEAD PyVarObject ob_base; typedef struct { PyObject ob_base; Py_ssize_t ob_size; /* Number of items in variable part */ } PyVarObject; typedef struct _object { _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; struct _typeobject *ob_type; } PyObject;
從上面的數(shù)據(jù)結(jié)構(gòu)來看和 list 的數(shù)據(jù)結(jié)構(gòu)基本上差不多,最終的使用方法也差不多。將上面的結(jié)構(gòu)體展開之后,PyTupleObject 的結(jié)構(gòu)大致如下所示:
現(xiàn)在來解釋一下上面的各個(gè)字段的含義:
- Py_ssize_t,一個(gè)整型數(shù)據(jù)類型。
- ob_refcnt,表示對(duì)象的引用記數(shù)的個(gè)數(shù),這個(gè)對(duì)于垃圾回收很有用處,后面我們分析虛擬機(jī)中垃圾回收部分在深入分析。
- ob_type,表示這個(gè)對(duì)象的數(shù)據(jù)類型是什么,在 python 當(dāng)中有時(shí)候需要對(duì)數(shù)據(jù)的數(shù)據(jù)類型進(jìn)行判斷比如 isinstance, type 這兩個(gè)關(guān)鍵字就會(huì)使用到這個(gè)字段。
- ob_size,這個(gè)字段表示這個(gè)元組當(dāng)中有多少個(gè)元素。
- ob_item,這是一個(gè)指針,指向真正保存 python 對(duì)象數(shù)據(jù)的地址,大致的內(nèi)存他們之間大致的內(nèi)存布局如下所示:
需要注意的是元組的數(shù)組大小是不能夠進(jìn)行更改的,這一點(diǎn)和 list 不一樣,我們可以注意到在 list 的數(shù)據(jù)結(jié)構(gòu)當(dāng)中還有一個(gè) allocated 字段,但是在元組當(dāng)中是沒有的,這主要是因?yàn)樵M的數(shù)組大小是固定的,而列表的數(shù)組大小是可以更改的。
元組操作函數(shù)源碼剖析
創(chuàng)建元組
首先我們需要了解一下在 cpython 內(nèi)部關(guān)于元組內(nèi)存分配的問題,首先和 list 一樣,在 cpython 當(dāng)中對(duì)于分配的好的元組進(jìn)行釋放的時(shí)候,并不會(huì)直接進(jìn)行釋放,而是會(huì)先保存下來,當(dāng)下次又有元組申請(qǐng)內(nèi)存的時(shí)候,直接將這塊內(nèi)存進(jìn)行返回即可。
在 cpython 內(nèi)部會(huì)進(jìn)行緩存的元組大小為 20,如果元組的長度為 0 - 19 那么在申請(qǐng)分配內(nèi)存之后釋放并不會(huì)直接釋放,而是將其先保存下來,下次有需求的時(shí)候直接分配,而不需要申請(qǐng)。在 cpython 內(nèi)部,相關(guān)的定義如下所示:
static PyTupleObject *free_list[PyTuple_MAXSAVESIZE]; static int numfree[PyTuple_MAXSAVESIZE];
- free_list,保存指針——指向被釋放的元組。
- numfree,對(duì)應(yīng)的下標(biāo)表示元組當(dāng)中元素的個(gè)數(shù),numfree[i] 表示有 i 個(gè)元素的元組的個(gè)數(shù)。
下面是新建 tuple 對(duì)象的源程序:
PyObject * PyTuple_New(Py_ssize_t size) { PyTupleObject *op; Py_ssize_t i; if (size < 0) { PyErr_BadInternalCall(); return NULL; } #if PyTuple_MAXSAVESIZE > 0 // 如果申請(qǐng)一個(gè)空的元組對(duì)象 當(dāng)前的 free_list 當(dāng)中是否存在空元組對(duì)象 如果存在則直接返回 if (size == 0 && free_list[0]) k op = free_list[0]; Py_INCREF(op); return (PyObject *) op; } // 如果元組的對(duì)象元素個(gè)數(shù)小于 20 而且對(duì)應(yīng)的 free_list 當(dāng)中還有余下的元組對(duì)象 則不需要進(jìn)行內(nèi)存申請(qǐng)直接返回 if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) { free_list[size] = (PyTupleObject *) op->ob_item[0]; numfree[size]--; /* Inline PyObject_InitVar */ _Py_NewReference((PyObject *)op); // _Py_NewReference 這個(gè)宏是將對(duì)象 op 的引用計(jì)數(shù)設(shè)置成 1 } else #endif { /* Check for overflow */ // 如果元組的元素個(gè)數(shù)大或者等于 20 或者 當(dāng)前 free_list 當(dāng)中沒有沒有剩余的對(duì)象則需要進(jìn)行內(nèi)存申請(qǐng) if ((size_t)size > ((size_t)PY_SSIZE_T_MAX - sizeof(PyTupleObject) - sizeof(PyObject *)) / sizeof(PyObject *)) { // 如果元組長度大于某個(gè)值直接報(bào)內(nèi)存錯(cuò)誤 return PyErr_NoMemory(); } // 申請(qǐng)?jiān)M大小的內(nèi)存空間 op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size); if (op == NULL) return NULL; } // 初始化內(nèi)存空間 for (i=0; i < size; i++) op->ob_item[i] = NULL; #if PyTuple_MAXSAVESIZE > 0 // 因?yàn)?size == 0 的元組不會(huì)進(jìn)行修改操作 因此可以直接將這個(gè)申請(qǐng)到的對(duì)象放到 free_list 當(dāng)中以備后續(xù)使用 if (size == 0) { free_list[0] = op; ++numfree[0]; Py_INCREF(op); /* extra INCREF so that this is never freed */ } #endif _PyObject_GC_TRACK(op); // _PyObject_GC_TRACK 這個(gè)宏是將對(duì)象 op 將入到垃圾回收隊(duì)列當(dāng)中 return (PyObject *) op; }
新建元組對(duì)象的流程如下所示:
- 查看 free_list 當(dāng)中是否已經(jīng)存在空閑的元組,如果有則直接進(jìn)行返回。
- 如果沒有,則進(jìn)行內(nèi)存分配,然后將申請(qǐng)的內(nèi)存空間進(jìn)行初始化操作。
- 如果 size == 0,則可以將新分配的元組對(duì)象放到 free_list 當(dāng)中。
查看元組的長度
這個(gè)功能比較簡單,直接只用 cpython 當(dāng)中的宏 Py_SIZE 即可。他的宏定義為 #define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size)。
static Py_ssize_t tuplelength(PyTupleObject *a) { return Py_SIZE(a); }
元組當(dāng)中是否包含數(shù)據(jù)
這個(gè)其實(shí)和 list 一樣,就是遍歷元組當(dāng)中的數(shù)據(jù),然后進(jìn)行比較即可。
static int tuplecontains(PyTupleObject *a, PyObject *el) { Py_ssize_t i; int cmp; for (i = 0, cmp = 0 ; cmp == 0 && i < Py_SIZE(a); ++i) cmp = PyObject_RichCompareBool(el, PyTuple_GET_ITEM(a, i), Py_EQ); return cmp; }
獲取和設(shè)置元組中的數(shù)據(jù)
這兩個(gè)方法也比較簡單,首先檢查數(shù)據(jù)類型是不是元組類型,然后判斷是否越界,之后就返回?cái)?shù)據(jù),或者設(shè)置對(duì)應(yīng)的數(shù)據(jù)。
這里在設(shè)置數(shù)據(jù)數(shù)據(jù)的時(shí)候需要注意一點(diǎn)的是,當(dāng)設(shè)置新的數(shù)據(jù)的時(shí)候,原來的 python 對(duì)象引用計(jì)數(shù)需要減去一,同理如果設(shè)置沒有成功的話傳入的新的數(shù)據(jù)的引用計(jì)數(shù)也需要減去一。
PyObject * PyTuple_GetItem(PyObject *op, Py_ssize_t i) { if (!PyTuple_Check(op)) { PyErr_BadInternalCall(); return NULL; } if (i < 0 || i >= Py_SIZE(op)) { PyErr_SetString(PyExc_IndexError, "tuple index out of range"); return NULL; } return ((PyTupleObject *)op) -> ob_item[i]; } int PyTuple_SetItem(PyObject *op, Py_ssize_t i, PyObject *newitem) { PyObject *olditem; PyObject **p; if (!PyTuple_Check(op) || op->ob_refcnt != 1) { Py_XDECREF(newitem); PyErr_BadInternalCall(); return -1; } if (i < 0 || i >= Py_SIZE(op)) { Py_XDECREF(newitem); PyErr_SetString(PyExc_IndexError, "tuple assignment index out of range"); return -1; } p = ((PyTupleObject *)op) -> ob_item + i; olditem = *p; *p = newitem; Py_XDECREF(olditem); return 0; }
釋放元組內(nèi)存空間
當(dāng)我們?cè)谶M(jìn)行垃圾回收的時(shí)候,判定一個(gè)對(duì)象的引用計(jì)數(shù)等于 0 的時(shí)候就需要釋放這塊內(nèi)存空間(相當(dāng)于析構(gòu)函數(shù)),下面就是釋放 tuple 內(nèi)存空間的函數(shù)。
static void tupledealloc(PyTupleObject *op) { Py_ssize_t i; Py_ssize_t len = Py_SIZE(op); PyObject_GC_UnTrack(op); // PyObject_GC_UnTrack 將對(duì)象從垃圾回收隊(duì)列當(dāng)中移除 Py_TRASHCAN_SAFE_BEGIN(op) if (len > 0) { i = len; while (--i >= 0) // 將這個(gè)元組指向的對(duì)象的引用計(jì)數(shù)減去一 Py_XDECREF(op->ob_item[i]); #if PyTuple_MAXSAVESIZE > 0 // 如果這個(gè)元組對(duì)象滿足加入 free_list 的條件,則將這個(gè)元組對(duì)象加入到 free_list 當(dāng)中 if (len < PyTuple_MAXSAVESIZE && numfree[len] < PyTuple_MAXFREELIST && Py_TYPE(op) == &PyTuple_Type) { op->ob_item[0] = (PyObject *) free_list[len]; numfree[len]++; free_list[len] = op; goto done; /* return */ } #endif } Py_TYPE(op)->tp_free((PyObject *)op); done: Py_TRASHCAN_SAFE_END(op) }
將元組的內(nèi)存空間回收的時(shí)候,主要有以下幾個(gè)步驟:
- 將元組對(duì)象從垃圾回收鏈表當(dāng)中移除。
- 將元組指向的所有對(duì)象的引用計(jì)數(shù)減一。
- 判斷元組是否滿足保存到 free_list 當(dāng)中的條件,如果滿足就將他加入到 free_list 當(dāng)中去,否則回收這塊內(nèi)存。加入到 free_list 當(dāng)中整個(gè)元組當(dāng)中 ob_item 指向變化如下所示:
如果不能夠?qū)⑨尫诺脑M對(duì)象加入到 free_list 當(dāng)中,否則將內(nèi)存釋放回收。
總結(jié)
在本篇文章當(dāng)中主要介紹了在 cpython 當(dāng)中是如何實(shí)現(xiàn) tuple 的,以及相關(guān)的數(shù)據(jù)結(jié)構(gòu)和一些基本的使用函數(shù),最后簡單談了關(guān)于元組內(nèi)存釋放的問題,這里面還是涉及一些其他的知識(shí)點(diǎn),不能夠在這篇文章進(jìn)行分析,在本文內(nèi)存分配主要是聚焦在元組身上,主要是分析內(nèi)存分配和 tuple 的 free_list 是如何交互的。
到此這篇關(guān)于深入理解Python虛擬機(jī)中元組(tuple)的實(shí)現(xiàn)原理及源碼的文章就介紹到這了,更多相關(guān)Python虛擬機(jī)元組內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python使用scrapy爬取陽光熱線問政平臺(tái)過程解析
這篇文章主要介紹了Python使用scrapy爬取陽光熱線問政平臺(tái)過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08詳解如何利用Pytest?Cache?Fixture實(shí)現(xiàn)測(cè)試結(jié)果緩存
這篇文章主要為大家詳細(xì)介紹了如何利用Pytest?Cache?Fixture實(shí)現(xiàn)測(cè)試結(jié)果緩存,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-09-09Python并發(fā)編程多進(jìn)程,多線程及GIL全局解釋器鎖
這篇文章主要介紹了Python并發(fā)編程多進(jìn)程,多線程及GIL全局解釋器鎖,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-07-07新版pycharm配置運(yùn)行參數(shù)的教程/pycharm2023
這篇文章主要介紹了新版pycharm配置運(yùn)行參數(shù)的教程/pycharm2023,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01使用Python實(shí)現(xiàn)七大排序算法的代碼實(shí)例
這篇文章主要介紹了使用Python實(shí)現(xiàn)七大排序算法的代碼實(shí)例,所謂排序,就是使一串記錄,按照其中的某個(gè)或某些關(guān)鍵字的大小,遞增或遞減的排列起來的操作,需要的朋友可以參考下2023-07-07python 實(shí)現(xiàn)提取某個(gè)索引中某個(gè)時(shí)間段的數(shù)據(jù)方法
今天小編就為大家分享一篇python 實(shí)現(xiàn)提取某個(gè)索引中某個(gè)時(shí)間段的數(shù)據(jù)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-02-02