亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Python對象是怎么被調用的

發布時間:2023-05-19 16:31:15 來源:億速云 閱讀:131 作者:iii 欄目:編程語言

今天小編給大家分享一下Python對象是怎么被調用的的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

楔子

我們知道對象被創建,主要有兩種方式,一種是通過Python/C API,另一種是通過調用類型對象。對于內置類型的實例對象而言,這兩種方式都是支持的,比如列表,我們即可以通過[]創建,也可以通過list(),前者是Python/C API,后者是調用類型對象。

但對于自定義類的實例對象而言,我們只能通過調用類型對象的方式來創建。而一個對象如果可以被調用,那么這個對象就是callable,否則就不是callable。

而決定一個對象是不是callable,就取決于其對應的類型對象中是否定義了某個方法。如果從 Python 的角度看的話,這個方法就是 __call__,從解釋器角度看的話,這個方法就是 tp_call。

從 Python 的角度看對象的調用

調用 int、str、tuple 可以創建一個整數、字符串、元組,調用自定義的類也可以創建出相應的實例對象,說明類型對象是可調用的,也就是callable。那么這些類型對象(int、str、tuple、class等等)的類型對象(type)內部一定有 __call__ 方法。

# int可以調用
# 那么它的類型對象、也就是元類(type), 內部一定有__call__方法
print(hasattr(type, "__call__"))# True
# 而調用一個對象,等價于調用其類型對象的 __call__ 方法
# 所以 int(3.14)實際就等價于如下
print(type.__call__(int, 3.14))# 3

注意:這里描述的可能有一些繞,我們說 int、str、float 這些都是類型對象(簡單來說就是類),而 123、"你好"、3.14 是其對應的實例對象,這些都沒問題。但type是不是類型對象,顯然是的,雖然我們稱呼它為元類,但它也是類型對象,如果 print(type) 顯示的也是一個類。

那么相對 type 而言,int、str、float 是不是又成了實例對象呢?因為它們的類型是 type。

所以 class 具有二象性:

  • 如果站在實例對象(如:123、"satori"、[]、3.14)的角度上,它是類型對象

  • 如果站在 type 的角度上,它是實例對象

同理 type 的類型是也是 type,那么 type 既是 type 的類型對象,type 也是 type 的實例對象。雖然這里描述的會有一些繞,但應該不難理解,并且為了避免后續的描述出現歧義,這里我們做一個申明:

  • 整數、浮點數、字符串等等,我們稱之為實例對象

  • int、float、str、dict,以及我們自定義的類,我們稱之為類型對象

  • type 雖然也是類型對象,但我們稱它為元類

所以 type 的內部有 __call__ 方法,那么說明類型對象都是可調用的,因為調用類型對象就是調用 type 的 __call__ 方法。而實例對象能否調用就不一定了,這取決于它的類型對象中是否定義了 __call__ 方法,因為調用一個對象,本質上是執行其類型對象內部的 __call__ 方法。

class A:
 pass
a = A()
# 因為我們自定義的類 A 里面沒有 __call__
# 所以 a 是不可以被調用的
try:
 a()
except Exception as e:
 # 告訴我們 A 的實例對象不可以被調用
 print(e)# 'A' object is not callable
# 如果我們給 A 設置了一個 __call__
type.__setattr__(A, "__call__", lambda self: "這是__call__")
# 發現可以調用了
print(a())# 這是__call__

我們看到這就是動態語言的特性,即便在類創建完畢之后,依舊可以通過type進行動態設置,而這在靜態語言中是不支持的。所以type是所有類的元類,它控制了我們自定義類的生成過程,type這個古老而又強大的類可以讓我們玩出很多新花樣。

但是對于內置的類,type是不可以對其動態增加、刪除或者修改屬性的,因為內置的類在底層是靜態定義好的。因為從源碼中我們看到,這些內置的類、包括元類,它們都是PyTypeObject對象,在底層已經被聲明為全局變量了,或者說它們已經作為靜態類存在了。所以type雖然是所有類型對象的元類,但是只有在面對我們自定義的類,type才具有增刪改的能力。

而且我們也解釋過,Python 的動態性是解釋器將字節碼翻譯成 C 代碼的時候動態賦予的,因此給類動態設置屬性或方法只適用于動態類,也就是在 py 文件中使用 class 關鍵字定義的類。

而對于靜態類、或者編寫擴展模塊時定義的擴展類(兩者是等價的),它們在編譯之后已經是指向 C 一級的數據結構了,不需要再被解釋器解釋了,因此解釋器自然也就無法在它們身上動手腳,畢竟彪悍的人生不需要解釋。

try:
 type.__setattr__(dict, "__call__", lambda self: "這是__call__")
except Exception as e:
 print(e)# can't set attributes of built-in/extension type 'dict'

我們看到拋異常了,提示我們不可以給內置/擴展類型dict設置屬性,因為它們繞過了解釋器解釋執行這一步,所以其屬性不能被動態設置。

同理其實例對象亦是如此,靜態類的實例對象也不可以動態設置屬性:

class Girl:
 pass
g = Girl()
g.name = "古明地覺"
# 實例對象我們也可以手動設置屬性
print(g.name)# 古明地覺
lst = list()
try:
 lst.name = "古明地覺"
except Exception as e:
 # 但是內置類型的實例對象是不可以的
 print(e)# 'list' object has no attribute 'name'

可能有人奇怪了,為什么列表不行呢?答案是內置類型的實例對象沒有__dict__屬性字典,因為相關屬性或方法底層已經定義好了,不可以動態添加。如果我們自定義類的時候,設置了__slots__,那么效果和內置的類是相同的。

當然了,我們后面會介紹如何通過動態修改解釋器來改變這一點,舉個栗子,不是說靜態類無法動態設置屬性嗎?下面我就來打自己臉:

import gc
try:
 type.__setattr__(list, "ping", "pong")
except TypeError as e:
 print(e)# can't set attributes of built-in/extension type 'list'
# 我們看到無法設置,那么我們就來改變這一點
attrs = gc.get_referents(tuple.__dict__)[0]
attrs["ping"] = "pong"
print(().ping)# pong
attrs["append"] = lambda self, item: self + (item,)
print(
 ().append(1).append(2).append(3)
)# (1, 2, 3)

我臉腫了。好吧,其實這只是我們玩的一個小把戲,當我們介紹完整個 CPython 的時候,會來專門聊一聊如何動態修改解釋器。比如:讓元組變得可修改,讓 Python 真正利用多核等等。

從解釋器的角度看對象的調用

我們以內置類型 float 為例,我們說創建一個 PyFloatObject,可以通過3.14或者float(3.14)的方式。前者使用Python/C API創建,3.14直接被解析為 C 一級數據結構,也就是PyFloatObject實例;后者使用類型對象創建,通過對float進行一個調用、將3.14作為參數,最終也得到指向C一級數據結構PyFloatObject實例。

Python/C API的創建方式我們已經很清晰了,就是根據值來推斷在底層應該對應哪一種數據結構,然后直接創建即可。我們重點看一下通過類型調用來創建實例對象的方式。

如果一個對象可以被調用,它的類型對象中一定要有tp_call(更準確的說成員tp_call的值是一個函數指針,不可以是0),而PyFloat_Type是可以調用的,這就說明PyType_Type內部的tp_call是一個函數指針,這在Python的層面上我們已經驗證過了,下面我們再來通過源碼看一下。

//typeobject.c
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 */
 //... /* tp_hash */
 (ternaryfunc)type_call, /* tp_call */
 //...
}

我們看到在實例化PyType_Type的時候PyTypeObject內部的成員tp_call被設置成了type_call。這是一個函數指針,當我們調用PyFloat_Type的時候,會觸發這個type_call指向的函數。

因此 float(3.14) 在C的層面上等價于:

(&PyFloat_Type) -> ob_type -> tp_call(&PyFloat_Type, args, kwargs);
// 即:
(&PyType_Type) -> tp_call(&PyFloat_Type, args, kwargs);
// 而在創建 PyType_Type 的時候,給 tp_call 成員傳遞的是 type_call
// 因此最終相當于
type_call(&PyFloat_Type, args, kwargs)

如果用 Python 來演示這一過程的話:

# float(3.14),等價于
f1 = float.__class__.__call__(float, 3.14)
# 等價于
f2 = type.__call__(float, 3.14)
print(f1, f2)# 3.14 3.14

這就是 float(3.14) 的秘密,相信list、dict在實例化的時候是怎么做的,你已經猜到了,做法是相同的。

# lst = list("abcd")
lst = list.__class__.__call__(list, "abcd")
print(lst)# ['a', 'b', 'c', 'd']
# dct = dict([("name", "古明地覺"), ("age", 17)])
dct = dict.__class__.__call__(dict, [("name", "古明地覺"), ("age", 17)])
print(dct)# {'name': '古明地覺', 'age': 17}

最后我們來圍觀一下 type_call 函數,我們說 type 的 __call__ 方法,在底層對應的是 type_call 函數,它位于Object/typeobject.c中。

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
 // 如果我們調用的是 float
 // 那么顯然這里的 type 就是 &PyFloat_Type

 // 這里是聲明一個PyObject *
 // 顯然它是要返回的實例對象的指針
 PyObject *obj;

 // 這里會檢測 tp_new是否為空,tp_new是什么估計有人已經猜到了
 // 我們說__call__對應底層的tp_call
 // 顯然__new__對應底層的tp_new,這里是為實例對象分配空間
 if (type->tp_new == NULL) {
 // tp_new 是一個函數指針,指向具體的構造函數
 // 如果 tp_new 為空,說明它沒有構造函數
 // 因此會報錯,表示無法創建其實例
 PyErr_Format(PyExc_TypeError,
"cannot create '%.100s' instances",
type->tp_name);
 return NULL;
 }

 //通過tp_new分配空間
 //此時實例對象就已經創建完畢了,這里會返回其指針
 obj = type->tp_new(type, args, kwds);
 //類型檢測,暫時不用管
 obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
 if (obj == NULL)
 return NULL;
 //我們說這里的參數type是類型對象,但也可以是元類
 //元類也是由PyTypeObject結構體實例化得到的
 //元類在調用的時候執行的依舊是type_call
 //所以這里是檢測type指向的是不是PyType_Type
 //如果是的話,那么實例化得到的obj就不是實例對象了,而是類型對象
 //要單獨檢測一下
 if (type == &PyType_Type &&
 PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
 (kwds == NULL ||
(PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))
 return obj;
 //tp_new應該返回相應類型對象的實例對象(的指針)
 //但如果不是,就直接將這里的obj返回
 //此處這么做可能有點難理解,我們一會細說
 if (!PyType_IsSubtype(Py_TYPE(obj), type))
 return obj;

 //拿到obj的類型
 type = Py_TYPE(obj);
 //執行 tp_init
 //顯然這個tp_init就是__init__函數
 //這與Python中類的實例化過程是一致的。
 if (type->tp_init != NULL) {
 //將tp_new返回的對象作為self,執行 tp_init
 int res = type->tp_init(obj, args, kwds);
 if (res < 0) {
 //執行失敗,將引入計數減1,然后將obj設置為NULL
 assert(PyErr_Occurred());
 Py_DECREF(obj);
 obj = NULL;
 }
 else {
 assert(!PyErr_Occurred());
 }
 }
 //返回obj
 return obj;
}

因此從上面我們可以看到關鍵的部分有兩個:

  • 調用類型對象的 tp_new 指向的函數為實例對象申請內存

  • 調用 tp_init 指向的函數為實例對象進行初始化,也就是設置屬性

所以這對應Python中的__new__和__init__,我們說__new__是為實例對象開辟一份內存,然后返回指向這片內存(對象)的指針,并且該指針會自動傳遞給__init__中的self。

class Girl:
 def __new__(cls, name, age):
 print("__new__方法執行啦")
 # 寫法非常固定
 # 調用object.__new__(cls)就會創建Girl的實例對象
 # 因此這里的cls指的就是這里的Girl,注意:一定要返回
 # 因為__new__會將自己的返回值交給__init__中的self
 return object.__new__(cls)
 def __init__(self, name, age):
 print("__init__方法執行啦")
 self.name = name
 self.age = age
g = Girl("古明地覺", 16)
print(g.name, g.age)
"""
__new__方法執行啦
__init__方法執行啦
古明地覺 16
"""

__new__里面的參數要和__init__里面的參數保持一致,因為我們會先執行__new__,然后解釋器會將__new__的返回值和我們傳遞的參數組合起來一起傳遞給__init__。因此__new__里面的參數除了cls之外,一般都會寫*args和**kwargs。

然后再回過頭來看一下type_call中的這幾行代碼:

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
 //......
 //......
 if (!PyType_IsSubtype(Py_TYPE(obj), type))
 return obj;

 //......
 //......
}

我們說tp_new應該返回該類型對象的實例對象,而且一般情況下我們是不寫__new__的,會默認執行。但是我們一旦重寫了,那么必須要手動返回object.__new__(cls)。可如果我們不返回,或者返回其它的話,會怎么樣呢?

class Girl:
 def __new__(cls, *args, **kwargs):
 print("__new__方法執行啦")
 instance = object.__new__(cls)
 # 打印看看instance到底是個什么東東
 print("instance:", instance)
 print("type(instance):", type(instance))

 # 正確做法是將instance返回
 # 但是我們不返回, 而是返回個 123
 return 123
 def __init__(self, name, age):
 print("__init__方法執行啦")
g = Girl()
"""
__new__方法執行啦
instance:type(instance):"""

這里面有很多可以說的點,首先就是 __init__ 里面需要兩個參數,但是我們沒有傳,卻還不報錯。原因就在于這個 __init__ 壓根就沒有執行,因為 __new__ 返回的不是 Girl 的實例對象。

通過打印 instance,我們知道了object.__new__(cls) 返回的就是 cls 的實例對象,而這里的cls就是Girl這個類本身。我們必須要返回instance,才會執行對應的__init__,否則__new__直接就返回了。我們在外部來打印一下創建的實例對象吧,看看結果:

class Girl:
 def __new__(cls, *args, **kwargs):
 return 123
 def __init__(self, name, age):
 print("__init__方法執行啦")
g = Girl()
print(g, type(g))# 123

我們看到打印的是123,所以再次總結一些tp_new和tp_init之間的區別,當然也對應__new__和__init__的區別:

  • tp_new:為該類型對象的實例對象申請內存,在Python的__new__方法中通過object.__new__(cls)的方式申請,然后將其返回

  • tp_init:tp_new的返回值會自動傳遞給self,然后為self綁定相應的屬性,也就是進行實例對象的初始化

但如果tp_new返回的不是對應類型的實例對象的指針,比如type_call中第一個參數接收的&PyFloat_Type,但是tp_new中返回的卻是PyLongObject *,所以此時就不會執行tp_init。

以上面的代碼為例,我們Girl中的__new__應該返回Girl的實例對象才對,但實際上返回了整型,因此類型不一致,所以不會執行__init__。

下面我們可以做總結了,通過類型對象去創建實例對象的整體流程如下:
  • 第一步:獲取類型對象的類型對象,說白了就是元類,執行元類的 tp_call 指向的函數,即 type_call

  • 第二步:type_call 會調用該類型對象的 tp_new 指向的函數,如果 tp_new 為 NULL,那么會到 tp_base 指定的父類里面去尋找 tp_new。在新式類當中,所有的類都繼承自 object,因此最終會執行 object 的 __new__。然后通過訪問對應類型對象中的 tp_basicsize 信息,這個信息記錄著該對象的實例對象需要占用多大的內存,繼而完成申請內存的操作

  • 調用type_new 創建完對象之后,就會進行實例對象的初始化,會將指向這片空間的指針交給 tp_init,但前提是 tp_new 返回的實例對象的類型要一致。

所以都說 Python 在實例化的時候會先調用 __new__ 方法,再調用 __init__ 方法,相信你應該知道原因了,因為在源碼中先調用 tp_new、再調用的 tp_init。

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
 //調用__new__方法, 拿到其返回值
 obj = type->tp_new(type, args, kwds);
 if (type->tp_init != NULL) {
 //將__new__返回的實例obj,和args、kwds組合起來
 //一起傳給 __init__
 //其中 obj 會傳給 self,
 int res = type->tp_init(obj, args, kwds);
 //......
 return obj;
}

所以源碼層面表現出來的,和我們在 Python 層面看到的是一樣的。

以上就是“Python對象是怎么被調用的”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

抚顺市| 江孜县| 沧州市| 隆安县| 峡江县| 江华| 万年县| 乌兰浩特市| 宣武区| 中方县| 永宁县| 武平县| 名山县| 布尔津县| 龙南县| 清流县| 博兴县| 上栗县| 萨嘎县| 青铜峡市| 元朗区| 凤山市| 巴马| 南川市| 鱼台县| 乃东县| 五莲县| 泗洪县| 西吉县| 上饶县| 乳源| 仙桃市| 清苑县| 略阳县| 高密市| 密山市| 达日县| 巴林右旗| 洞口县| 肃北| 富裕县|