當調(diào)用一個 Python 對象時,背后都經(jīng)歷了哪些過程?
楔子
在上一篇文章中,我們分析了對象是如何創(chuàng)建的,主要有兩種方式,一種是通過特定類型 API,另一種是通過調(diào)用類型對象。
對于內(nèi)置類型的實例對象而言,這兩種方式都是支持的,比如列表,我們既可以通過 [ ] 創(chuàng)建,也可以通過 list() 創(chuàng)建,前者是列表的特定類型 API,后者是調(diào)用類型對象。
但對于自定義類的實例對象而言,我們只能通過調(diào)用類型對象的方式來創(chuàng)建。一個對象如果可以被調(diào)用,那么這個對象就是 callable,否則就不是 callable。而決定一個對象是不是 callable,則取決于它的類型對象。
- 從 Python 的角度看,如果對象是 callable,那么它的類型對象一定實現(xiàn)了 __call__ 函數(shù);
- 從解釋器的角度看,如果對象是 callable,那么它的類型對象的 tp_call 字段一定不為空。
從 Python 的角度看對象的調(diào)用
調(diào)用 int 可以創(chuàng)建一個整數(shù),調(diào)用 str 可以創(chuàng)建一個字符串,調(diào)用 tuple 可以創(chuàng)建一個元組,調(diào)用自定義的類也可以創(chuàng)建出相應的實例對象,這就說明類型對象是可調(diào)用的,也就是 callable。
既然類型對象可調(diào)用,那么類型對象的類型對象(type)內(nèi)部一定實現(xiàn)了 __call__ 函數(shù)。
# int 可以調(diào)用,那么它的類型對象、也就是元類(type)
# 內(nèi)部一定實現(xiàn)了 __call__ 函數(shù)
print(hasattr(type, "__call__")) # True
# 而調(diào)用一個對象,等價于調(diào)用其類型對象的 __call__ 函數(shù)
# 所以 int(2.71) 實際就等價于如下
print(type.__call__(int, 2.71)) # 2
我們說 int、str、float 這些都是類型對象(簡單來說就是類),而 123、"你好"、2.71 是其對應的實例對象,這些都沒問題。但相對 type 而言,int、str、float 是不是又成了實例對象呢?因為它們的類型是 type。
所以 class 具有二象性:
- 如果站在實例對象(如:123、"satori"、2.71)的角度上,它是類型對象;
- 如果站在 type 的角度上,它是實例對象;
同理,由于 type 的類型還是 type,那么 type 既是 type 的類型對象,type 也是 type 的實例對象。雖然這里描述的有一些繞,但應該不難理解,而為了避免后續(xù)的描述出現(xiàn)歧義,這里我們做一個申明:
- 整數(shù)、浮點數(shù)、字符串、列表等等,我們稱之為實例對象
- int、float、str、dict,以及自定義的類,我們稱之為類型對象
- type 雖然也是類型對象,但我們稱它為元類
由于 type 的內(nèi)部定義了 __call__ 函數(shù),那么說明類型對象都是可調(diào)用的,因為調(diào)用類型對象就是調(diào)用元類 type 的 __call__ 函數(shù)。而實例對象能否調(diào)用就不一定了,這取決于它的類型對象是否定義了 __call__ 函數(shù),因為調(diào)用一個對象,本質(zhì)上是調(diào)用其類型對象內(nèi)部的 __call__ 函數(shù)。
class A:
pass
a = A()
# 因為自定義的類 A 里面沒有 __call__
# 所以 a 是不可以被調(diào)用的
try:
a()
except Exception as e:
# 告訴我們 A 的實例對象不可以被調(diào)用
print(e) # 'A' object is not callable
# 如果我們給 A 設置了一個 __call__
type.__setattr__(A, "__call__", lambda self: "這是__call__")
# 發(fā)現(xiàn)可以調(diào)用了
print(a()) # 這是__call__
這就是動態(tài)語言的特性,即便在類創(chuàng)建完畢之后,依舊可以通過 type 進行動態(tài)設置,而這在靜態(tài)語言中是不支持的。所以 type 是所有類的元類,它控制了自定義類的生成過程,因此 type 這個古老而又強大的類可以讓我們玩出很多新花樣。
但對于內(nèi)置的類,type 是不可以對其動態(tài)增加、刪除或者修改屬性的,因為內(nèi)置的類在底層是靜態(tài)定義好的。從源碼中我們看到,這些內(nèi)置的類、包括元類,它們都是 PyTypeObject 對象,在底層已經(jīng)被聲明為全局變量了,或者說它們已經(jīng)作為靜態(tài)類存在了。所以 type 雖然是所有類型對象的類型,但只有在面對我們自定義的類,type 才具有對屬性進行增刪改的能力。
而且在上一篇文章中我們也解釋過,Python 的動態(tài)性是解釋器將字節(jié)碼翻譯成 C 代碼的時候動態(tài)賦予的,因此給類對象動態(tài)設置屬性只適用于動態(tài)類,也就是在 py 文件中使用 class 關鍵字定義的類。
而對于靜態(tài)類,它們在編譯之后已經(jīng)是指向 C 一級的數(shù)據(jù)結(jié)構(gòu)了,不需要再被解釋器解釋了,因此解釋器自然也就無法在它們身上動手腳,畢竟彪悍的人生不需要解釋。
try:
type.__setattr__(dict, "ping", "pong")
except Exception as e:
print(e)
"""
cannot set 'ping' attribute of immutable type 'dict'
"""
try:
type.__setattr__(list, "ping", "pong")
except Exception as e:
print(e)
"""
cannot set 'ping' attribute of immutable type 'list'
"""
同理其實例對象亦是如此,靜態(tài)類的實例對象也不可以動態(tài)設置屬性:
lst = list()
try:
lst.name = "古明地覺"
except Exception as e:
print(e) # 'list' object has no attribute 'name'
在介紹 PyTypeObject 結(jié)構(gòu)體的時候我們說過,靜態(tài)類的實例對象可以綁定哪些屬性,已經(jīng)寫死在 tp_members 字段里面了。
從解釋器的角度看對象的調(diào)用
以內(nèi)置類型 list 為例,我們說創(chuàng)建一個列表,可以通過 [ ] 或者 list() 的方式。前者使用列表的特定類型 API 創(chuàng)建,[ ] 會被直接解析成 C 一級的數(shù)據(jù)結(jié)構(gòu),也就是 PyListObject 實例;后者使用類型對象創(chuàng)建,對 list 進行調(diào)用,最終也得到指向 C 一級的數(shù)據(jù)結(jié)構(gòu) PyListObject 實例。
第一種方式我們已經(jīng)很熟悉了,就是根據(jù)值來推斷在底層應該對應哪一種數(shù)據(jù)結(jié)構(gòu),然后直接創(chuàng)建即可,因為解釋器對內(nèi)置的數(shù)據(jù)結(jié)構(gòu)了如指掌。我們重點來看第二種方式,也就是通過調(diào)用類型對象去創(chuàng)建實例對象。
如果一個對象可以被調(diào)用,那么它的類型對象中一定要有 tp_call,更準確的說是 tp_call 字段的值是一個具體的函數(shù)指針,而不是 0。由于 PyList_Type 是可以調(diào)用的,這就說明 PyType_Type 內(nèi)部的 tp_call 是一個函數(shù)指針,這在 Python 的層面我們已經(jīng)驗證過了,下面再來通過源碼看一下。
圖片
在創(chuàng)建 PyType_Type 的時候,PyTypeObject 內(nèi)部的 tp_call 字段被設置成了 type_call。所以當我們調(diào)用 PyList_Type 的時候,會執(zhí)行 type_call 函數(shù)。
因此 list() 在 C 的層面上等價于:
(&PyList_Type)->ob_type->tp_call(&PyList_Type, args, kwargs);
// 即:
(&PyType_Type)->tp_call(&PyList_Type, args, kwargs);
// 而在創(chuàng)建 PyType_Type 的時候,給 tp_call 字段傳遞的是 type_call
// 因此最終相當于
type_call(&PyList_Type, args, kwargs)
如果用 Python 來演示這一過程的話:
# 以 list("abcd") 為例,它等價于
lst1 = list.__class__.__call__(list, "abcd")
# 等價于
lst2 = type.__call__(list, "abcd")
print(lst1) # ['a', 'b', 'c', 'd']
print(lst2) # ['a', 'b', 'c', 'd']
這就是 list() 的秘密,相信其它類型在實例化的時候是怎么做的,你已經(jīng)知道了,做法是相同的。
# dct = dict([("name", "古明地覺"), ("age", 17)])
dct = dict.__class__.__call__(
dict, [("name", "古明地覺"), ("age", 17)]
)
print(dct) # {'name': '古明地覺', 'age': 17}
# buf = bytes("hello world", encoding="utf-8")
buf = bytes.__class__.__call__(
bytes, "hello world", encoding="utf-8"
)
print(buf) # b'hello world'
當然,目前還沒有結(jié)束,我們還需要看一下 type_call 的源碼實現(xiàn)。
type_call 源碼解析
調(diào)用類型對象,本質(zhì)上會調(diào)用 type.__call__,在底層對應 type_call 函數(shù),因為 PyType_Type 的 tp_call 字段被設置成了 type_call。當然調(diào)用 type 也是如此,因為 type 的類型還是 type。
那么這個 type_call 都做了哪些事情呢?
static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
// 參數(shù) type 表示類型對象或者元類,假設調(diào)用的是 list,那么它就是 &PyList_Type
// 參數(shù) args 和 kwds 表示位置參數(shù)和關鍵字參數(shù),args 是元組,kwds 是字典
// 創(chuàng)建的實例對象,當然也可能是類型對象,取決于參數(shù) type
PyObject *obj;
// 線程狀態(tài)對象,后續(xù)介紹線程的時候會細說
// 此處的線程狀態(tài)對象是用來設置異常的
PyThreadState *tstate = _PyThreadState_GET();
// 如果參數(shù) type 是 &PyType_Type,也就是 Python 中的元類
if (type == &PyType_Type) {
// 那么它只能接收一個位置參數(shù)(查看對象類型)或三個位置參數(shù)(動態(tài)創(chuàng)建類)
Py_ssize_t nargs = PyTuple_GET_SIZE(args); // 獲取位置參數(shù)的個數(shù)
// 如果位置參數(shù)個數(shù)為 1,并且沒有傳遞關鍵字參數(shù),那么直接返回對象的類型
if (nargs == 1 && (kwds == NULL || !PyDict_GET_SIZE(kwds))) {
// Py_TYPE 負責獲取對象類型,因此相當于 type(args[0])
obj = (PyObject *) Py_TYPE(PyTuple_GET_ITEM(args, 0));
// 增加引用計數(shù),返回 obj
return Py_NewRef(obj);
}
// 如果位置參數(shù)的個數(shù)不等于 1,那么一定等于 3
if (nargs != 3) {
PyErr_SetString(PyExc_TypeError,
"type() takes 1 or 3 arguments");
return NULL;
}
}
// 接下來執(zhí)行類型對象(也可能是元類)的 tp_new,也就是 __new__
// 如果不存在,那么會報錯,而在 Python 中見到的報錯信息就是這里指定的
if (type->tp_new == NULL) {
_PyErr_Format(tstate, PyExc_TypeError,
"cannot create '%s' instances", type->tp_name);
return NULL;
}
// 執(zhí)行類型對象的 __new__
obj = type->tp_new(type, args, kwds);
// 檢測調(diào)用是否正常,如果調(diào)用正常,那么 obj 一定指向一個合法的 PyObject
// 而如果 obj 為 NULL,則表示執(zhí)行出錯,此時解釋器會拋出異常
obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL);
if (obj == NULL)
return NULL;
// __new__ 執(zhí)行完之后該執(zhí)行啥了,顯然是 __init__,但需要先做一個檢測
// 如果 __new__ 返回的實例對象的類型不是當前類型,那么直接返回,不再執(zhí)行 __init__
// 比如自定義 class A,那么在 __new__ 里面應該返回 A 的實例對象,但假設返回個 123
// 由于返回值的類型不是當前類型,那么不再執(zhí)行初始化函數(shù) __init__
if (!PyObject_TypeCheck(obj, type))
return obj;
// 走到這里說明類型一致,那么執(zhí)行 __init__,將 obj、args、kwds 一起傳過去
type = Py_TYPE(obj);
if (type->tp_init != NULL) {
int res = type->tp_init(obj, args, kwds);
if (res < 0) {
assert(_PyErr_Occurred(tstate));
Py_SETREF(obj, NULL);
}
else {
assert(!_PyErr_Occurred(tstate));
}
}
// 返回創(chuàng)建的對象 obj
return obj;
}
所以整個過程就三步:
- 如果傳遞的是元類,并且只有一個參數(shù),那么直接返回對象的類型;
- 否則先調(diào)用 tp_new 為實例對象申請內(nèi)存;
- 再調(diào)用 tp_init(如果有)進行初始化,設置對象屬性;
所以這對應了 Python 中的 __new__ 和 __init__,其中 __new__ 負責為實例對象開辟一份內(nèi)存,然后返回指向?qū)ο蟮闹羔?,并且該指針會自動傳遞給 __init__ 中的 self。
class Girl:
def __new__(cls, name, age):
print("__new__ 方法執(zhí)行啦")
# 調(diào)用 object.__new__(cls) 創(chuàng)建 Girl 的實例對象
# 然后該對象的指針會自動傳遞給 __init__ 中的 self
return object.__new__(cls)
def __init__(self, name, age):
print("__init__ 方法執(zhí)行啦")
self.name = name
self.age = age
g = Girl("古明地覺", 16)
print(g.name, g.age)
"""
__new__ 方法執(zhí)行啦
__init__ 方法執(zhí)行啦
古明地覺 16
"""
__new__ 里面的參數(shù)要和 __init__ 里面的參數(shù)保持一致,因為會先執(zhí)行 __new__,然后解釋器再將 __new__ 的返回值和傳遞的參數(shù)組合起來一起傳給 __init__。因此從這個角度講,設置屬性完全可以在 __new__ 里面完成。
class Girl:
def __new__(cls, name, age):
self = object.__new__(cls)
self.name = name
self.age = age
return self
g = Girl("古明地覺", 16)
print(g.name, g.age)
"""
古明地覺 16
"""
這樣也是沒問題的,不過 __new__ 一般只負責創(chuàng)建實例,設置屬性應該交給 __init__ 來做,畢竟一個是構(gòu)造函數(shù)、一個是初始化函數(shù),各司其職。另外由于 __new__ 里面不負責初始化,那么它的參數(shù)除了 cls 之外,一般都會寫成 *args 和 **kwargs。
然后再回過頭來看一下 type_call 中的這兩行代碼:
圖片
tp_new 應該返回該類型對象的實例對象,而且一般情況下我們是不重寫 __new__ 的,會默認執(zhí)行 object 的 __new__。但如果我們重寫了,那么必須要手動返回 object.__new__(cls)??扇绻覀儾环祷?,或者返回其它的話,會怎么樣呢?
class Girl:
def __new__(cls, *args, **kwargs):
print("__new__ 方法執(zhí)行啦")
instance = object.__new__(cls)
# 打印看看 instance 到底是個啥
print("instance:", instance)
print("type(instance):", type(instance))
# 正確做法是將 instance 返回
# 但是我們不返回,而是返回一個整數(shù) 123
return 123
def __init__(self, name, age):
print("__init__ 方法執(zhí)行啦")
g = Girl()
"""
__new__ 方法執(zhí)行啦
instance: <__main__.Girl object at 0x0000019A2B7270A0>
type(instance): <class '__main__.Girl'>
"""
這里面有很多可以說的點,首先就是 __init__ 里面需要兩個參數(shù),但是我們沒有傳,卻還不報錯。原因就在于這個 __init__ 壓根就沒有執(zhí)行,因為 __new__ 返回的不是 Girl 的實例對象。
通過打印 instance,我們知道了 object.__new__(cls) 返回的就是 cls 的實例對象,而這里的 cls 就是 Girl 這個類本身。所以我們必須要返回 instance,才會自動執(zhí)行相應的 __init__。
我們在外部來打印一下創(chuàng)建的實例對象吧,看看結(jié)果:
class Girl:
def __new__(cls, *args, **kwargs):
return 123
def __init__(self, name, age):
print("__init__ 方法執(zhí)行啦")
g = Girl()
print(g)
"""
123
"""
我們看到打印的結(jié)果是 123,所以再次總結(jié)一下 tp_new 和 tp_init 之間的區(qū)別,當然也對應 __new__ 和 __init__ 的區(qū)別:
- tp_new:為實例對象申請內(nèi)存,底層會調(diào)用 tp_alloc,至于對象的大小則記錄在 tp_basicsize 字段中,而在 Python 里面則是調(diào)用 object.__new__(cls),然后返回;
- tp_init:tp_new 的返回值會自動傳遞給 self,然后為 self 綁定相應的屬性,也就是進行實例對象的初始化;
但如果 tp_new 返回的對象的類型不對,比如 type_call 的第一個參數(shù)接收的是 &PyList_Type,但 tp_new 返回的卻是 PyTupleObject *,那么此時就不會執(zhí)行 tp_init。
對應上面的 Python 代碼就是,Girl 的 __new__ 應該返回 Girl 的實例對象(指針)才對,但卻返回了整數(shù),因此類型不一致,不會執(zhí)行 __init__。
所以都說類在實例化的時候會先調(diào)用 __new__,再調(diào)用 __init__,相信你應該知道原因了,因為在源碼中先調(diào)用 tp_new,再調(diào)用 tp_init。所以源碼層面表現(xiàn)出來的,和我們在 Python 層面看到的是一樣的。
小結(jié)
到此,我們就從 Python 和解釋器兩個層面解釋了對象是如何調(diào)用的,更準確的說我們是從解釋器的角度對 Python 層面的知識進行了驗證,通過 tp_new 和 tp_init 的關系,來了解 __new__ 和 __init__ 的關系。
當然對象調(diào)用還不止目前說的這么簡單,更多的細節(jié)隱藏在了幕后。后續(xù)我們會循序漸進,一點點地揭開它的面紗,并且在這個過程中還會不斷地學習到新的東西。比如說,實例對象在調(diào)用方法的時候會自動將實例本身作為參數(shù)傳遞給 self,那么它為什么會傳遞呢?解釋器在背后又做了什么工作呢?這些在之后的文章中都會詳細說明。