楔子
我們知道對象被創(chuàng)建,主要有兩種方式,一種是通過Python/C API,另一種是通過調(diào)用類型對象。對于內(nèi)置類型的實例對象而言,這兩種方式都是支持的,比如列表,我們即可以通過[]創(chuàng)建,也可以通過list(),前者是Python/C API,后者是調(diào)用類型對象。
但對于自定義類的實例對象而言,我們只能通過調(diào)用類型對象的方式來創(chuàng)建。而一個對象如果可以被調(diào)用,那么這個對象就是callable,否則就不是callable。
而決定一個對象是不是callable,就取決于其對應的類型對象中是否定義了某個方法。如果從 Python 的角度看的話,這個方法就是 __call__,從解釋器角度看的話,這個方法就是 tp_call。
從 Python 的角度看對象的調(diào)用
調(diào)用 int、str、tuple 可以創(chuàng)建一個整數(shù)、字符串、元組,調(diào)用自定義的類也可以創(chuàng)建出相應的實例對象,說明類型對象是可調(diào)用的,也就是callable。那么這些類型對象(int、str、tuple、class等等)的類型對象(type)內(nèi)部一定有 __call__ 方法。
# int可以調(diào)用 # 那么它的類型對象、也就是元類(type), 內(nèi)部一定有__call__方法 print(hasattr(type, "__call__"))# True # 而調(diào)用一個對象,等價于調(diào)用其類型對象的 __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 的實例對象。雖然這里描述的會有一些繞,但應該不難理解,并且為了避免后續(xù)的描述出現(xiàn)歧義,這里我們做一個申明:
- 整數(shù)、浮點數(shù)、字符串等等,我們稱之為實例對象
- int、float、str、dict,以及我們自定義的類,我們稱之為類型對象
- type 雖然也是類型對象,但我們稱它為元類
所以 type 的內(nèi)部有 __call__ 方法,那么說明類型對象都是可調(diào)用的,因為調(diào)用類型對象就是調(diào)用 type 的 __call__ 方法。而實例對象能否調(diào)用就不一定了,這取決于它的類型對象中是否定義了 __call__ 方法,因為調(diào)用一個對象,本質(zhì)上是執(zhí)行其類型對象內(nèi)部的 __call__ 方法。
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 設(shè)置了一個 __call__ type.__setattr__(A, "__call__", lambda self: "這是__call__") # 發(fā)現(xiàn)可以調(diào)用了 print(a())# 這是__call__
我們看到這就是動態(tài)語言的特性,即便在類創(chuàng)建完畢之后,依舊可以通過type進行動態(tài)設(shè)置,而這在靜態(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)設(shè)置屬性或方法只適用于動態(tài)類,也就是在 py 文件中使用 class 關(guān)鍵字定義的類。
而對于靜態(tài)類、或者編寫擴展模塊時定義的擴展類(兩者是等價的),它們在編譯之后已經(jīng)是指向 C 一級的數(shù)據(jù)結(jié)構(gòu)了,不需要再被解釋器解釋了,因此解釋器自然也就無法在它們身上動手腳,畢竟彪悍的人生不需要解釋。
try: type.__setattr__(dict, "__call__", lambda self: "這是__call__") except Exception as e: print(e)# can't set attributes of built-in/extension type 'dict'
我們看到拋異常了,提示我們不可以給內(nèi)置/擴展類型dict設(shè)置屬性,因為它們繞過了解釋器解釋執(zhí)行這一步,所以其屬性不能被動態(tài)設(shè)置。
同理其實例對象亦是如此,靜態(tài)類的實例對象也不可以動態(tài)設(shè)置屬性:
class Girl: pass g = Girl() g.name = "古明地覺" # 實例對象我們也可以手動設(shè)置屬性 print(g.name)# 古明地覺 lst = list() try: lst.name = "古明地覺" except Exception as e: # 但是內(nèi)置類型的實例對象是不可以的 print(e)# 'list' object has no attribute 'name'
可能有人奇怪了,為什么列表不行呢?答案是內(nèi)置類型的實例對象沒有__dict__屬性字典,因為相關(guān)屬性或方法底層已經(jīng)定義好了,不可以動態(tài)添加。如果我們自定義類的時候,設(shè)置了__slots__,那么效果和內(nèi)置的類是相同的。
當然了,我們后面會介紹如何通過動態(tài)修改解釋器來改變這一點,舉個栗子,不是說靜態(tài)類無法動態(tài)設(shè)置屬性嗎?下面我就來打自己臉:
import gc try: type.__setattr__(list, "ping", "pong") except TypeError as e: print(e)# can't set attributes of built-in/extension type 'list' # 我們看到無法設(shè)置,那么我們就來改變這一點 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 的時候,會來專門聊一聊如何動態(tài)修改解釋器。比如:讓元組變得可修改,讓 Python 真正利用多核等等。
從解釋器的角度看對象的調(diào)用
我們以內(nèi)置類型 float 為例,我們說創(chuàng)建一個 PyFloatObject,可以通過3.14或者float(3.14)的方式。前者使用Python/C API創(chuàng)建,3.14直接被解析為 C 一級數(shù)據(jù)結(jié)構(gòu),也就是PyFloatObject實例;后者使用類型對象創(chuàng)建,通過對float進行一個調(diào)用、將3.14作為參數(shù),最終也得到指向C一級數(shù)據(jù)結(jié)構(gòu)PyFloatObject實例。
Python/C API的創(chuàng)建方式我們已經(jīng)很清晰了,就是根據(jù)值來推斷在底層應該對應哪一種數(shù)據(jù)結(jié)構(gòu),然后直接創(chuàng)建即可。我們重點看一下通過類型調(diào)用來創(chuàng)建實例對象的方式。
如果一個對象可以被調(diào)用,它的類型對象中一定要有tp_call(更準確的說成員tp_call的值是一個函數(shù)指針,不可以是0),而PyFloat_Type是可以調(diào)用的,這就說明PyType_Type內(nèi)部的tp_call是一個函數(shù)指針,這在Python的層面上我們已經(jīng)驗證過了,下面我們再來通過源碼看一下。
//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內(nèi)部的成員tp_call被設(shè)置成了type_call。這是一個函數(shù)指針,當我們調(diào)用PyFloat_Type的時候,會觸發(fā)這個type_call指向的函數(shù)。
因此 float(3.14) 在C的層面上等價于:
(&PyFloat_Type) -> ob_type -> tp_call(&PyFloat_Type, args, kwargs); // 即: (&PyType_Type) -> tp_call(&PyFloat_Type, args, kwargs); // 而在創(chuàng)建 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在實例化的時候是怎么做的,你已經(jīng)猜到了,做法是相同的。
# 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 函數(shù),我們說 type 的 __call__ 方法,在底層對應的是 type_call 函數(shù),它位于Object/typeobject.c中。
static PyObject * type_call(PyTypeObject *type, PyObject *args, PyObject *kwds) { // 如果我們調(diào)用的是 float // 那么顯然這里的 type 就是 &PyFloat_Type // 這里是聲明一個PyObject * // 顯然它是要返回的實例對象的指針 PyObject *obj; // 這里會檢測 tp_new是否為空,tp_new是什么估計有人已經(jīng)猜到了 // 我們說__call__對應底層的tp_call // 顯然__new__對應底層的tp_new,這里是為實例對象分配空間 if (type->tp_new == NULL) { // tp_new 是一個函數(shù)指針,指向具體的構(gòu)造函數(shù) // 如果 tp_new 為空,說明它沒有構(gòu)造函數(shù) // 因此會報錯,表示無法創(chuàng)建其實例 PyErr_Format(PyExc_TypeError, "cannot create '%.100s' instances", type->tp_name); return NULL; } //通過tp_new分配空間 //此時實例對象就已經(jīng)創(chuàng)建完畢了,這里會返回其指針 obj = type->tp_new(type, args, kwds); //類型檢測,暫時不用管 obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL); if (obj == NULL) return NULL; //我們說這里的參數(shù)type是類型對象,但也可以是元類 //元類也是由PyTypeObject結(jié)構(gòu)體實例化得到的 //元類在調(diào)用的時候執(zhí)行的依舊是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); //執(zhí)行 tp_init //顯然這個tp_init就是__init__函數(shù) //這與Python中類的實例化過程是一致的。 if (type->tp_init != NULL) { //將tp_new返回的對象作為self,執(zhí)行 tp_init int res = type->tp_init(obj, args, kwds); if (res < 0) { //執(zhí)行失敗,將引入計數(shù)減1,然后將obj設(shè)置為NULL assert(PyErr_Occurred()); Py_DECREF(obj); obj = NULL; } else { assert(!PyErr_Occurred()); } } //返回obj return obj; }
因此從上面我們可以看到關(guān)鍵的部分有兩個:
- 調(diào)用類型對象的 tp_new 指向的函數(shù)為實例對象申請內(nèi)存
- 調(diào)用 tp_init 指向的函數(shù)為實例對象進行初始化,也就是設(shè)置屬性
所以這對應Python中的__new__和__init__,我們說__new__是為實例對象開辟一份內(nèi)存,然后返回指向這片內(nèi)存(對象)的指針,并且該指針會自動傳遞給__init__中的self。
class Girl: def __new__(cls, name, age): print("__new__方法執(zhí)行啦") # 寫法非常固定 # 調(diào)用object.__new__(cls)就會創(chuàng)建Girl的實例對象 # 因此這里的cls指的就是這里的Girl,注意:一定要返回 # 因為__new__會將自己的返回值交給__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__里面的參數(shù)除了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__的,會默認執(zhí)行。但是我們一旦重寫了,那么必須要手動返回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返回 # 但是我們不返回, 而是返回個 123 return 123 def __init__(self, name, age): print("__init__方法執(zhí)行啦") g = Girl() """ __new__方法執(zhí)行啦 instance: <__main__.Girl object at 0x000002C0F16FA1F0> type(instance): <class '__main__.Girl'> """
這里面有很多可以說的點,首先就是 __init__ 里面需要兩個參數(shù),但是我們沒有傳,卻還不報錯。原因就在于這個 __init__ 壓根就沒有執(zhí)行,因為 __new__ 返回的不是 Girl 的實例對象。
通過打印 instance,我們知道了object.__new__(cls) 返回的就是 cls 的實例對象,而這里的cls就是Girl這個類本身。我們必須要返回instance,才會執(zhí)行對應的__init__,否則__new__直接就返回了。我們在外部來打印一下創(chuàng)建的實例對象吧,看看結(jié)果:
class Girl: def __new__(cls, *args, **kwargs): return 123 def __init__(self, name, age): print("__init__方法執(zhí)行啦") g = Girl() print(g, type(g))# 123 <class 'int'>
我們看到打印的是123,所以再次總結(jié)一些tp_new和tp_init之間的區(qū)別,當然也對應__new__和__init__的區(qū)別:
- tp_new:為該類型對象的實例對象申請內(nèi)存,在Python的__new__方法中通過object.__new__(cls)的方式申請,然后將其返回
- tp_init:tp_new的返回值會自動傳遞給self,然后為self綁定相應的屬性,也就是進行實例對象的初始化
但如果tp_new返回的不是對應類型的實例對象的指針,比如type_call中第一個參數(shù)接收的&PyFloat_Type,但是tp_new中返回的卻是PyLongObject *,所以此時就不會執(zhí)行tp_init。
以上面的代碼為例,我們Girl中的__new__應該返回Girl的實例對象才對,但實際上返回了整型,因此類型不一致,所以不會執(zhí)行__init__。
下面我們可以做總結(jié)了,通過類型對象去創(chuàng)建實例對象的整體流程如下:
- 第一步:獲取類型對象的類型對象,說白了就是元類,執(zhí)行元類的 tp_call 指向的函數(shù),即 type_call
- 第二步:type_call 會調(diào)用該類型對象的 tp_new 指向的函數(shù),如果 tp_new 為 NULL,那么會到 tp_base 指定的父類里面去尋找 tp_new。在新式類當中,所有的類都繼承自 object,因此最終會執(zhí)行 object 的 __new__。然后通過訪問對應類型對象中的 tp_basicsize 信息,這個信息記錄著該對象的實例對象需要占用多大的內(nèi)存,繼而完成申請內(nèi)存的操作
- 調(diào)用type_new 創(chuàng)建完對象之后,就會進行實例對象的初始化,會將指向這片空間的指針交給 tp_init,但前提是 tp_new 返回的實例對象的類型要一致。
所以都說 Python 在實例化的時候會先調(diào)用 __new__ 方法,再調(diào)用 __init__ 方法,相信你應該知道原因了,因為在源碼中先調(diào)用 tp_new、再調(diào)用的 tp_init。
static PyObject * type_call(PyTypeObject *type, PyObject *args, PyObject *kwds) { //調(diào)用__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; }
所以源碼層面表現(xiàn)出來的,和我們在 Python 層面看到的是一樣的。
小結(jié)
到此,我們就從 Python 和解釋器兩個層面了解了對象是如何調(diào)用的,更準確的說我們是從解釋器的角度對 Python 層面的知識進行了驗證,通過 tp_new 和 tp_init 的關(guān)系,來了解 __new__ 和 __init__ 的關(guān)系。
另外,對象調(diào)用遠不止我們目前說的這么簡單,更多的細節(jié)隱藏在了幕后,只不過現(xiàn)在沒辦法將其一次性全部挖掘出來。
以上是源碼探秘:Python 中對象是如何被調(diào)用的?的詳細內(nèi)容。更多信息請關(guān)注PHP中文網(wǎng)其他相關(guān)文章!

熱AI工具

Undress AI Tool
免費脫衣服圖片

Undresser.AI Undress
人工智能驅(qū)動的應用程序,用于創(chuàng)建逼真的裸體照片

AI Clothes Remover
用于從照片中去除衣服的在線人工智能工具。

Clothoff.io
AI脫衣機

Video Face Swap
使用我們完全免費的人工智能換臉工具輕松在任何視頻中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的代碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
功能強大的PHP集成開發(fā)環(huán)境

Dreamweaver CS6
視覺化網(wǎng)頁開發(fā)工具

SublimeText3 Mac版
神級代碼編輯軟件(SublimeText3)

用戶語音輸入通過前端JavaScript的MediaRecorderAPI捕獲并發(fā)送至PHP后端;2.PHP將音頻保存為臨時文件后調(diào)用STTAPI(如Google或百度語音識別)轉(zhuǎn)換為文本;3.PHP將文本發(fā)送至AI服務(如OpenAIGPT)獲取智能回復;4.PHP再調(diào)用TTSAPI(如百度或Google語音合成)將回復轉(zhuǎn)為語音文件;5.PHP將語音文件流式返回前端播放,完成交互。整個流程由PHP主導數(shù)據(jù)流轉(zhuǎn)與錯誤處理,確保各環(huán)節(jié)無縫銜接。

要實現(xiàn)PHP結(jié)合AI進行文本糾錯與語法優(yōu)化,需按以下步驟操作:1.選擇適合的AI模型或API,如百度、騰訊API或開源NLP庫;2.通過PHP的curl或Guzzle調(diào)用API并處理返回結(jié)果;3.在應用中展示糾錯信息并允許用戶選擇是否采納;4.使用php-l和PHP_CodeSniffer進行語法檢測與代碼優(yōu)化;5.持續(xù)收集反饋并更新模型或規(guī)則以提升效果。選擇AIAPI時應重點評估準確率、響應速度、價格及對PHP的支持。代碼優(yōu)化應遵循PSR規(guī)范、合理使用緩存、避免循環(huán)查詢、定期審查代碼,并借助X

使用Seaborn的jointplot可快速可視化兩個變量間的關(guān)系及各自分布;2.基礎(chǔ)散點圖通過sns.jointplot(data=tips,x="total_bill",y="tip",kind="scatter")實現(xiàn),中心為散點圖,上下和右側(cè)顯示直方圖;3.添加回歸線和密度信息可用kind="reg",并結(jié)合marginal_kws設(shè)置邊緣圖樣式;4.數(shù)據(jù)量大時推薦kind="hex",用

要將AI情感計算技術(shù)融入PHP應用,核心是利用云服務AIAPI(如Google、AWS、Azure)進行情感分析,通過HTTP請求發(fā)送文本并解析返回的JSON結(jié)果,將情感數(shù)據(jù)存入數(shù)據(jù)庫,從而實現(xiàn)用戶反饋的自動化處理與數(shù)據(jù)洞察。具體步驟包括:1.選擇適合的AI情感分析API,綜合考慮準確性、成本、語言支持和集成復雜度;2.使用Guzzle或curl發(fā)送請求,存儲情感分數(shù)、標簽及強度等信息;3.構(gòu)建可視化儀表盤,支持優(yōu)先級排序、趨勢分析、產(chǎn)品迭代方向和用戶細分;4.應對技術(shù)挑戰(zhàn),如API調(diào)用限制、數(shù)

字符串列表可用join()方法合并,如''.join(words)得到"HelloworldfromPython";2.數(shù)字列表需先用map(str,numbers)或[str(x)forxinnumbers]轉(zhuǎn)為字符串后才能join;3.任意類型列表可直接用str()轉(zhuǎn)換為帶括號和引號的字符串,適用于調(diào)試;4.自定義格式可用生成器表達式結(jié)合join()實現(xiàn),如'|'.join(f"[{item}]"foriteminitems)輸出"[a]|[

pandas.melt()用于將寬格式數(shù)據(jù)轉(zhuǎn)為長格式,答案是通過指定id_vars保留標識列、value_vars選擇需融化的列、var_name和value_name定義新列名,1.id_vars='Name'表示Name列不變,2.value_vars=['Math','English','Science']指定要融化的列,3.var_name='Subject'設(shè)置原列名的新列名,4.value_name='Score'設(shè)置原值的新列名,最終生成包含Name、Subject和Score三列

pythoncanbeoptimizedFormized-formemory-boundoperationsbyreducingOverHeadThroughGenerator,有效dattratsures,andManagingObjectLifetimes.first,useGeneratorSInsteadoFlistSteadoflistSteadoFocessLargedAtasetSoneItematatime,desceedingingLoadeGingloadInterveringerverneDraineNterveingerverneDraineNterveInterveIntMory.second.second.second.second,Choos,Choos

安裝pyodbc:使用pipinstallpyodbc命令安裝庫;2.連接SQLServer:通過pyodbc.connect()方法,使用包含DRIVER、SERVER、DATABASE、UID/PWD或Trusted_Connection的連接字符串,分別支持SQL身份驗證或Windows身份驗證;3.查看已安裝驅(qū)動:運行pyodbc.drivers()并篩選含'SQLServer'的驅(qū)動名,確保使用如'ODBCDriver17forSQLServer'等正確驅(qū)動名稱;4.連接字符串關(guān)鍵參數(shù)
