Python對(duì)象的生命周期源碼學(xué)習(xí)
思考:
當(dāng)我們輸入這個(gè)語(yǔ)句的時(shí)候,Python內(nèi)部是如何去創(chuàng)建這個(gè)對(duì)象的?
a = 1.0
對(duì)象使用完畢,銷(xiāo)毀的時(shí)機(jī)又是怎么確定的呢?
下面,我們以一個(gè)基本類型float為例,來(lái)分析對(duì)象從創(chuàng)建到銷(xiāo)毀這整個(gè)生命周期中的行為。
1 C API
Python是用C寫(xiě)的,對(duì)外提供了API,讓用戶可以從C環(huán)境中與其交互,并且Python內(nèi)部也大量使用了這些API。C API分為兩類:泛型API以及特型API。
泛型API:與類型無(wú)關(guān),屬于抽象對(duì)象層,這類API的參數(shù)是PyObject *,即可以處理任意類型的對(duì)象。以PyObject_Print為例:
// 打印浮點(diǎn)對(duì)象 PyObject *fo = PyFloat_FromDouble(3.14); PyObject_Print(fo, stdout, 0); // 打印整數(shù)對(duì)象 PyObject *lo = PyLong_FromLong(100); PyObject_Print(lo, stdout, 0);
特型API:與類型相關(guān),屬于具體對(duì)象層,這類API只能作用于某種類型的對(duì)象
2 對(duì)象的創(chuàng)建
2.1 兩種創(chuàng)建對(duì)象的方式
Python內(nèi)部一般通過(guò)兩種方法創(chuàng)建對(duì)象:
通過(guò)C API,多用于內(nèi)建類型
以浮點(diǎn)類型為例,Python內(nèi)部提供PyFloat_FromDouble,這是一個(gè)特型C API,在這個(gè)接口內(nèi)部為PyFloatObject結(jié)構(gòu)體變量分配內(nèi)存,并初始化相關(guān)字段:
PyObject * PyFloat_FromDouble(double fval) { PyFloatObject *op = free_list; if (op != NULL) { free_list = (PyFloatObject *) Py_TYPE(op); numfree--; } else { op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject)); if (!op) return PyErr_NoMemory(); } /* Inline PyObject_New */ (void)PyObject_INIT(op, &PyFloat_Type); op->ob_fval = fval; return (PyObject *) op; }
通過(guò)類型對(duì)象,多用于自定義類型
對(duì)于自定義類型,Python就無(wú)法事先提供C API了,這種情況下就只能通過(guò)類型對(duì)象中包含的元數(shù)據(jù)(分配多少內(nèi)存,如何初始化等等)來(lái)創(chuàng)建實(shí)例對(duì)象。
由類型對(duì)象創(chuàng)建實(shí)例對(duì)象是一個(gè)更通用的流程,對(duì)于內(nèi)建類型,除了通過(guò)C API來(lái)創(chuàng)建對(duì)象意外,同樣也可以通過(guò)類型對(duì)象來(lái)創(chuàng)建。以浮點(diǎn)類型為例,我們通過(guò)類型對(duì)象float,創(chuàng)建了一個(gè)實(shí)例對(duì)象f:
f: float = float('3.123')
2.2 由類型對(duì)象創(chuàng)建實(shí)例對(duì)象
思考:既然我們可以通過(guò)類型對(duì)象來(lái)創(chuàng)建實(shí)例對(duì)象,那么類型對(duì)象中應(yīng)該存在相應(yīng)的接口。
在PyType_Type中找到了tp_call字段:
PyTypeObject PyType_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "type", /* tp_name */ sizeof(PyHeapTypeObject), /* tp_basicsize */ sizeof(PyMemberDef), /* tp_itemsize */ (destructor)type_dealloc, /* tp_dealloc */ // ... (ternaryfunc)type_call, /* tp_call */ // ... };
因此,float(‘3.123’)在C層面就等價(jià)于:
PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args. kwargs)
這里大家可以思考下為什么是PyFloat_Type.ob_type——因?yàn)槲覀冊(cè)趂loat(‘3.14’)中是通過(guò)float這個(gè)類型對(duì)象去創(chuàng)建一個(gè)浮點(diǎn)對(duì)象,而對(duì)象的通用方法是由它對(duì)應(yīng)的類型管理的,自然float的類型就是type,所以我們要找的就是type的tp_call字段。
type_call函數(shù)的C源碼:(只列出部分)
static PyObject * type_call(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyObject *obj; // ... obj = type->tp_new(type, args, kwds); obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL); if (obj == NULL) return NULL; // ... type = Py_TYPE(obj); if (type->tp_init != NULL) { int res = type->tp_init(obj, args, kwds); if (res < 0) { assert(PyErr_Occurred()); Py_DECREF(obj); obj = NULL; } else { assert(!PyErr_Occurred()); } } return obj; }
其中有兩個(gè)關(guān)鍵的步驟:(這兩個(gè)步驟大家應(yīng)該是很熟悉的)
- 調(diào)用類型對(duì)象的tp_new函數(shù)指針,用于申請(qǐng)內(nèi)存;
- 如果類型對(duì)象的tp_init函數(shù)指針不為空,則會(huì)對(duì)對(duì)象進(jìn)行初始化。
總結(jié):(以float為例)
- 調(diào)用float,Python最終執(zhí)行的是其類型對(duì)象type的tp_call指針指向的type_call函數(shù)。
- type_call函數(shù)調(diào)用float的tp_new函數(shù)為實(shí)例對(duì)象分配內(nèi)存空間。
- type_call函數(shù)必要時(shí)進(jìn)一步調(diào)用tp_init函數(shù)對(duì)實(shí)例對(duì)象進(jìn)行初始化。
圖示如下:
3 對(duì)象的多態(tài)性
通過(guò)類型對(duì)象創(chuàng)建實(shí)例對(duì)象,最后會(huì)落實(shí)到調(diào)用type_call函數(shù),其中保存具體對(duì)象時(shí),使用的是PyObject *obj,并沒(méi)有通過(guò)一個(gè)具體的對(duì)象(例如PyFloatObject)來(lái)保存。這樣做的好處是:可以實(shí)現(xiàn)更抽象的上層邏輯,而不用關(guān)心對(duì)象的實(shí)際類型和實(shí)現(xiàn)細(xì)節(jié)。(記得當(dāng)初從C語(yǔ)言的面向過(guò)程向Java中的面向?qū)ο筮^(guò)度的時(shí)候,應(yīng)該就是從結(jié)構(gòu)體)
以對(duì)象哈希值計(jì)算為例,有這樣一個(gè)函數(shù)接口:
Py_hash_t PyObject_Hash(PyObject *v) { // ... }
對(duì)于浮點(diǎn)數(shù)對(duì)象和整數(shù)對(duì)象:
PyObject *fo = PyFloatObject_FromDouble(3.14); PyObject_Hash(fo); PyObject *lo = PyLongObject_FromLong(100); PyObject_Hash(lo);
可以看到,對(duì)于浮點(diǎn)數(shù)對(duì)象和整數(shù)對(duì)象,我們計(jì)算對(duì)象的哈希值時(shí),調(diào)用的都是PyObject_Hash()這個(gè)函數(shù),但是對(duì)象類型不同,其行為是有區(qū)別的,哈希值計(jì)算也是如此。
那么在PyObject_Hash函數(shù)內(nèi)部是如何區(qū)分的呢?
PyObject_Hash()函數(shù)具體邏輯:
Py_hash_t PyObject_Hash(PyObject *v) { PyTypeObject *tp = Py_TYPE(v); if (tp->tp_hash != NULL) return (*tp->tp_hash)(v); /* To keep to the general practice that inheriting * solely from object in C code should work without * an explicit call to PyType_Ready, we implicitly call * PyType_Ready here and then check the tp_hash slot again */ if (tp->tp_dict == NULL) { if (PyType_Ready(tp) < 0) return -1; if (tp->tp_hash != NULL) return (*tp->tp_hash)(v); } /* Otherwise, the object can't be hashed */ return PyObject_HashNotImplemented(v); }
函數(shù)會(huì)首先通過(guò)Py_TYPE找到對(duì)象的類型,然后通過(guò)類型對(duì)象的tp_hash函數(shù)指針來(lái)調(diào)用對(duì)應(yīng)的哈希計(jì)算函數(shù)。
即:PyObject_Hash()函數(shù)根據(jù)對(duì)象的類型,調(diào)用不同的函數(shù)版本,這就是多態(tài)。
4 對(duì)象的行為
除了tp_hash字段,PyTypeObject結(jié)構(gòu)體還定義了很多函數(shù)指針,這些指針最終都會(huì)指向某個(gè)函數(shù),或者為空。我們可以把這些函數(shù)指針看作是類型對(duì)象中定義的操作,這些操作決定了對(duì)應(yīng)的實(shí)例對(duì)象在運(yùn)行時(shí)的行為。
雖然不同的類型對(duì)象中保存了對(duì)應(yīng)實(shí)例對(duì)象共有的行為,但是不同類型的對(duì)象也會(huì)存在一些共性。例如:整數(shù)對(duì)象和浮點(diǎn)數(shù)對(duì)象都支持加減乘除等擦歐總,元組對(duì)象和列表對(duì)象都支持下標(biāo)操作。因此,我們以行為為分類標(biāo)準(zhǔn),對(duì)對(duì)象進(jìn)行分類:
Python以此為依據(jù),為每個(gè)類別都定義了一個(gè)標(biāo)準(zhǔn)操作集:
- PyNumberMethods結(jié)構(gòu)體定義了數(shù)值型操作
- PySequenceMethods結(jié)構(gòu)體定義了序列型操作
- PyMappingMethods結(jié)構(gòu)體定義了關(guān)聯(lián)型操作
如果類型對(duì)象提供了相關(guān)的操作集,則對(duì)應(yīng)的實(shí)例對(duì)象就具備對(duì)應(yīng)的行為:
typedef struct _typeobject { PyObject_VAR_HEAD const char *tp_name; /* For printing, in format "<module>.<name>" */ Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ // ... PyNumberMethods *tp_as_number; PySequenceMethods *tp_as_sequence; PyMappingMethods *tp_as_mapping; // ... } PyTypeObject;
以float為例,類型對(duì)象PyFloat_Type的這三個(gè)字段是這樣初始化的:
PyTypeObject PyFloat_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "float", sizeof(PyFloatObject), // ... &float_as_number, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ // ... };
可以看到,只有tp_as_number非空,即float對(duì)象支持?jǐn)?shù)值型操作,不支持序列型操作和關(guān)聯(lián)型操作。
5 引用計(jì)數(shù)
在Python中,很多場(chǎng)景都涉及引用計(jì)數(shù)的調(diào)整:
- 變量賦值
- 函數(shù)參數(shù)傳遞
- 屬性操作
- 容器操作
引用計(jì)數(shù)是Python生命周期中很關(guān)鍵的一個(gè)知識(shí)點(diǎn),后續(xù)我會(huì)用一個(gè)單獨(dú)的章節(jié)來(lái)介紹,這里咱們先按下不表,更多關(guān)于Python對(duì)象生命周期的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
PyCharm代碼整體縮進(jìn),反向縮進(jìn)的方法
今天小編就為大家分享一篇PyCharm代碼整體縮進(jìn),反向縮進(jìn)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-06-06淺析python中的絕對(duì)導(dǎo)入和相對(duì)導(dǎo)入
這篇文章主要是想和大家簡(jiǎn)單聊聊python中絕對(duì)導(dǎo)入和相對(duì)導(dǎo)入的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以參考下2023-09-09Flask Paginate實(shí)現(xiàn)表格分頁(yè)的使用示例
flask_paginate是Flask框架的一個(gè)分頁(yè)擴(kuò)展,用于處理分頁(yè)相關(guān)的功能,本文就來(lái)介紹一下Flask Paginate實(shí)現(xiàn)表格分頁(yè)的使用示例,感興趣的可以了解一下2023-11-11使用Python實(shí)現(xiàn)繪制地圖的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何使用Python實(shí)現(xiàn)繪制地圖相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01Python使用add_subplot與subplot畫(huà)子圖操作示例
這篇文章主要介紹了Python使用add_subplot與subplot畫(huà)子圖操作,涉及Python使用matplotlib模塊進(jìn)行圖形繪制的相關(guān)操作技巧,需要的朋友可以參考下2018-06-06Python Pandas知識(shí)點(diǎn)之缺失值處理詳解
這篇文章主要給大家介紹了關(guān)于Pandas知識(shí)點(diǎn)之缺失值處理的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05Python cookbook(數(shù)據(jù)結(jié)構(gòu)與算法)實(shí)現(xiàn)對(duì)不原生支持比較操作的對(duì)象排序算法示例
這篇文章主要介紹了Python cookbook(數(shù)據(jù)結(jié)構(gòu)與算法)實(shí)現(xiàn)對(duì)不原生支持比較操作的對(duì)象排序算法,結(jié)合實(shí)例形式分析了Python針對(duì)類實(shí)例進(jìn)行排序相關(guān)操作技巧,需要的朋友可以參考下2018-03-03深入探究Python Numba庫(kù)編譯優(yōu)化利器
這篇文章主要為大家介紹了Python Numba庫(kù)編譯優(yōu)化利器深入探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Python Lambda函數(shù)使用總結(jié)詳解
這篇文章主要介紹了Python Lambda函數(shù)使用總結(jié)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12