<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
我們知道物件是如何被建立的,主要有兩種方式,一種是通過Python/C API,另一種是通過呼叫型別物件。對於內建型別的範例物件而言,這兩種方式都是支援的,比如列表,我們即可以通過[]建立,也可以通過list(),前者是Python/C API,後者是呼叫型別物件。
但對於自定義類的範例物件而言,我們只能通過呼叫型別物件的方式來建立。而一個物件如果可以被呼叫,那麼這個物件就是callable,否則就不是callable。
而決定一個物件是不是callable,就取決於其對應的型別物件中是否定義了某個方法。如果從 Python 的角度看的話,這個方法就是 __call__,從直譯器角度看的話,這個方法就是 tp_call。
呼叫 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 具有二象性:
同理 type 的型別是也是 type,那麼 type 既是 type 的型別物件,type 也是 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; }
因此從上面我們可以看到關鍵的部分有兩個:
所以這對應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__方法執行啦 """
__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: <__main__.Girl object at 0x000002C0F16FA1F0> type(instance): <class '__main__.Girl'> """
這裡面有很多可以說的點,首先就是 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 <class 'int'>
我們看到列印的是123,所以再次總結一些tp_new和tp_init之間的區別,當然也對應__new__和__init__的區別:
但如果tp_new返回的不是對應型別的範例物件的指標,比如type_call中第一個引數接收的&PyFloat_Type,但是tp_new中返回的卻是PyLongObject *,所以此時就不會執行tp_init。
以上面的程式碼為例,我們Girl中的__new__應該返回Girl的範例物件才對,但實際上返回了整型,因此型別不一致,所以不會執行__init__。
下面我們可以做總結了,通過型別物件去建立範例物件的整體流程如下:
呼叫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 和直譯器兩個層面瞭解了物件是如何呼叫的,更準確的說我們是從直譯器的角度對 Python 層面的知識進行了驗證,通過 tp_new 和 tp_init 的關係,來了解 new 和 init 的關係。
另外,物件呼叫遠不止我們目前說的這麼簡單,更多的細節隱藏在了幕後,只不過現在沒辦法將其一次性全部挖掘出來。後續我們會循序漸進,一點點揭開它什麼面紗,並且在這個過程中還會不斷地學習到新的東西。比如說,範例物件在呼叫方法的時候會自動將範例本身作為引數傳遞給 self,那麼它為什麼傳遞呢?直譯器在背後又做了什麼工作呢?這些我們就以後慢慢說吧。
到此這篇關於淺談Python中物件是如何被呼叫的的文章就介紹到這了,更多相關Python物件呼叫內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45