上一篇文章我們說(shuō)了 Python 函數(shù)的底層實(shí)現(xiàn),并且還演示了如何通過(guò)函數(shù)的類型對(duì)象自定義一個(gè)函數(shù),以及如何獲取函數(shù)的參數(shù)。雖然這在工作中沒(méi)有太大意義,但是可以讓我們深刻理解函數(shù)的行為。 那么接下來(lái)看看函數(shù)是如何調(diào)用的。 在介紹調(diào)用之前,我們需要補(bǔ)充一個(gè)知識(shí)點(diǎn)。 def foo(): pass
class A:
def foo(self): pass
print(type(foo)) # <class 'function'> print(type(A().foo)) # <class 'method'> print(type(sum)) # <class 'builtin_function_or_method'> print(type("".join)) # <class 'builtin_function_or_method'>
如果采用 Python 實(shí)現(xiàn),那么函數(shù)的類型是 function,方法的類型是 method。而如果采用原生的 C 實(shí)現(xiàn),那么函數(shù)和方法的類型都是 builtin_function_or_method。
關(guān)于方法,等我們介紹類的時(shí)候再說(shuō),先來(lái)看看函數(shù)。 所以函數(shù)分為兩種: Python 實(shí)現(xiàn)的函數(shù),在底層由 PyFunctionObject 結(jié)構(gòu)體實(shí)例表示,其類型對(duì)象 <class 'function'> 在底層由 PyFunction_Type 表示。 C 實(shí)現(xiàn)的函數(shù)(還有方法),在底層由 PyCFunctionObject 結(jié)構(gòu)體實(shí)例表示,其類型對(duì)象 <class 'builtin_function_or_method'> 在底層由 PyCFunction_Type 表示。
像我們使用 def 關(guān)鍵字定義的就是 Python 實(shí)現(xiàn)的函數(shù),而內(nèi)置函數(shù)則是 C 實(shí)現(xiàn)的函數(shù),它們?cè)诘讓訉?duì)應(yīng)不同的結(jié)構(gòu),因?yàn)?C 實(shí)現(xiàn)的函數(shù)可以有更快的執(zhí)行方式。 我們來(lái)調(diào)用一個(gè)函數(shù),看看它的字節(jié)碼是怎樣的。 import dis
code_string = """ def foo(a, b): return a + b
foo(1, 2) """ dis.dis(compile(code_string, "<file>", "exec"))
字節(jié)碼指令如下: 0 RESUME 0 # 加載 PyCodeObject 對(duì)象,壓入運(yùn)行時(shí)棧 2 LOAD_CONST 0 (<code object foo at 0x7f...>) # 從棧頂彈出 PyCodeObject 對(duì)象,構(gòu)建函數(shù) 4 MAKE_FUNCTION 0 # 將符號(hào) foo 和函數(shù)對(duì)象綁定起來(lái),存儲(chǔ)在名字空間中 6 STORE_NAME 0 (foo)
8 PUSH_NULL # 加載全局變量 foo,壓入運(yùn)行時(shí)棧 10 LOAD_NAME 0 (foo) # 加載常量 1,壓入運(yùn)行時(shí)棧 12 LOAD_CONST 1 (1) # 加載常量 2,壓入運(yùn)行時(shí)棧 14 LOAD_CONST 2 (2) # 彈出 foo 和參數(shù),進(jìn)行調(diào)用 # 指令參數(shù) 2,表示給調(diào)用的函數(shù)傳遞了兩個(gè)參數(shù) # 函數(shù)調(diào)用結(jié)束后,將返回值壓入棧中 16 CALL 2 # 因?yàn)闆](méi)有用變量保存,所以從棧頂彈出返回值并丟棄 24 POP_TOP # 隱式的 return None 26 RETURN_CONST 3 (None) # 函數(shù)內(nèi)部邏輯對(duì)應(yīng)的字節(jié)碼,比較簡(jiǎn)單,就不說(shuō)了 Disassembly of <code object foo at 0x7f6...>: 0 RESUME 0
2 LOAD_FAST 0 (a) 4 LOAD_FAST 1 (b) 6 BINARY_OP 0 (+) 10 RETURN_VALUE
我們看到函數(shù)調(diào)用使用的是 CALL 指令,那么這個(gè)指令都做了哪些事情呢? TARGET(CALL) { // ... // 運(yùn)行時(shí)棧從棧底到棧頂?shù)脑胤謩e是:NULL, 函數(shù), 參數(shù)1, 參數(shù) 2, ... // 至于為啥會(huì)有一個(gè) NULL,我們?cè)倏匆幌聞偛诺淖止?jié)碼指令就明白了 // 在 LOAD_NAME 將函數(shù)對(duì)象的指針壓入運(yùn)行時(shí)棧之前,先執(zhí)行了 PUSH_NULL // 所以棧底元素是 NULL,不過(guò)問(wèn)題又來(lái)了,為啥要往棧里面壓入一個(gè) NULL 呢 // PUSH_NULL 這個(gè)指令我們之前也見(jiàn)過(guò),只不過(guò)當(dāng)時(shí)沒(méi)有解釋 // 它是干嘛的,接下來(lái)你就會(huì)明白
// oparg 表示給函數(shù)傳遞的參數(shù)的個(gè)數(shù),所以 args 指向第一個(gè)參數(shù) PyObject **args = (stack_pointer - oparg); // 等價(jià)于 *(args - 1),顯然這是函數(shù) PyObject *callable = stack_pointer[-(1 + oparg)]; // *(args - 2) 毫無(wú)疑問(wèn)就是棧底元素 NULL // 但它卻被賦值為 method,難道和方法有關(guān)嗎? PyObject *method = stack_pointer[-(2 + oparg)]; PyObject *res; // 返回值 #line 2653 "Python/bytecodes.c" // 如果 method 不為 NULL,說(shuō)明執(zhí)行的不是普通的函數(shù),而是方法 // 所謂方法其實(shí)就是將函數(shù)和 self 綁定起來(lái)的結(jié)果 int is_meth = method != NULL; int total_args = oparg; // 總之現(xiàn)在我們明白為什么要壓入一個(gè) NULL 了,就是為了和方法調(diào)用保持統(tǒng)一 // 如果調(diào)用的是方法,那么棧里的元素就是:函數(shù), self, 參數(shù)1, 參數(shù)2, ... // 方法是對(duì)函數(shù)和 self 的綁定,調(diào)用方法本質(zhì)上還是在調(diào)用函數(shù) // 只不過(guò)調(diào)用的時(shí)候,會(huì)自動(dòng)傳遞 self,舉個(gè)例子 /* * class A: * def foo(self): * pass * * a = A() */ // 如果是 A.foo,那么拿到的就是普通的函數(shù) // 因?yàn)楹瘮?shù)定義在類里面,所以 A.foo 也叫類的成員函數(shù),但它依舊是一個(gè)普通的函數(shù) // 如果是 a.foo,那么拿到的就是方法,它會(huì)將 A.foo 和實(shí)例對(duì)象 a 自身綁定起來(lái) // 調(diào)用方法時(shí)會(huì)自動(dòng)傳遞 self,所以 a.foo() 本質(zhì)上就是 A.foo(a) if (is_meth) { // 當(dāng) is_meth 為真時(shí) callable = method; // method 才是要調(diào)用的 callable args--; // 此時(shí) self 變成了真正意義上的第一個(gè)參數(shù),因此 args-- total_args++; // 參數(shù)個(gè)數(shù)加 1,因此 total_args++ } // 通過(guò) PUSH_NULL,可以讓函數(shù)和方法的調(diào)用對(duì)應(yīng)同一個(gè)指令 // 當(dāng)然,即使不考慮方法,提前 PUSH 一個(gè) NULL 在邏輯上也是正確的 // 因?yàn)槿魏魏瘮?shù)都有返回值,執(zhí)行完之后要設(shè)置在棧頂?shù)奈恢?/span> // 而一開(kāi)始 PUSH 的 NULL 正好為返回值預(yù)留了空間 // ...
// 如果調(diào)用的函數(shù),那么棧里的元素是:NULL, 函數(shù), 參數(shù)1, 參數(shù)2, ... // 如果調(diào)用的方法,那么棧里的元素是:函數(shù), self, 參數(shù)1, 參數(shù)2, ... // 但對(duì)于方法而言,棧里的元素還有一種情況:NULL, 方法, 參數(shù)1, 參數(shù)2, ... // 對(duì)于這種情況,要將方法里面的函數(shù)和 self 提取出來(lái) // 所以當(dāng) is_meth 為 0,但 callable 的類型是 <class 'method'> 時(shí) if (!is_meth && Py_TYPE(callable) == &PyMethod_Type) { is_meth = 1; // 將 is_meth 設(shè)置為 1 args--; // args 依舊向前移動(dòng)一個(gè)位置 total_args++; // 參數(shù)總個(gè)數(shù)加 1 // 獲取方法里面的實(shí)例對(duì)象 PyObject *self = ((PyMethodObject *)callable)->im_self; // args 向前移動(dòng)一個(gè)位置之后,它指向了目前方法所在的位置 // 將該位置的值換成 self args[0] = Py_NewRef(self); // 獲取方法里面的函數(shù) method = ((PyMethodObject *)callable)->im_func; // 將 args 的前一個(gè)位置的值設(shè)置成函數(shù) args[-1] = Py_NewRef(method); Py_DECREF(callable); callable = method; // 所以之前棧里的元素是:NULL, 方法, 參數(shù)1, 參數(shù)2, ... // args 之前也指向`參數(shù)1`,但在 args-- 之后,便指向了`方法` // 等到將 args[0] 設(shè)置成 self,將 args[-1] 設(shè)置成函數(shù)之后 // 棧里的元素就變成了:函數(shù), self, 參數(shù)1, 參數(shù)2, ... } // 到這里為止,不管是調(diào)用函數(shù)還是調(diào)用方法,邏輯都變得統(tǒng)一了 // 此時(shí)變量 callable 指向?qū)嶋H要調(diào)用的函數(shù) // args 指向第一個(gè)參數(shù),total_args 表示參數(shù)的個(gè)數(shù) int positional_args = total_args - KWNAMES_LEN(); // 函數(shù)在初始化時(shí),它的 vectorcall 字段會(huì)被設(shè)置為 _PyFunction_Vectorcall // 所以對(duì)于函數(shù)來(lái)講,下面這個(gè)條件是成立的,因此可以被內(nèi)聯(lián) if (Py_TYPE(callable) == &PyFunction_Type && tstate->interp->eval_frame == NULL && ((PyFunctionObject *)callable)->vectorcall == _PyFunction_Vectorcall) { // 獲取 co_flags int code_flags = ((PyCodeObject*)PyFunction_GET_CODE(callable))->co_flags; // 如果是函數(shù)的 PyCodeObject,那么 local 名字空間指定為 NULL // 因?yàn)榫植孔兞坎皇菑?nbsp;local 名字空間中加載的,而是靜態(tài)訪問(wèn)的 PyObject *locals = code_flags & CO_OPTIMIZED ? NULL : \ Py_NewRef(PyFunction_GET_GLOBALS(callable)); // 在當(dāng)前棧幀之上創(chuàng)建新的棧幀,初始化相關(guān)字段 // 然后推入到虛擬機(jī)為其準(zhǔn)備的 C Stack 中 _PyInterpreterFrame *new_frame = _PyEvalFramePushAndInit( tstate, (PyFunctionObject *)callable, locals, args, positional_args, kwnames ); kwnames = NULL; // 將運(yùn)行時(shí)棧清空 STACK_SHRINK(oparg + 2); if (new_frame == NULL) { goto error; } JUMPBY(INLINE_CACHE_ENTRIES_CALL); frame->return_offset = 0; DISPATCH_INLINED(new_frame); } // 到這里 callable 不是一個(gè)普通的 Python 函數(shù),但它支持 vector 協(xié)議 // 進(jìn)行調(diào)用 res = PyObject_Vectorcall( callable, args, positional_args | PY_VECTORCALL_ARGUMENTS_OFFSET, kwnames); // ... kwnames = NULL; assert((res != NULL) ^ (_PyErr_Occurred(tstate) != NULL)); Py_DECREF(callable); for (int i = 0; i < total_args; i++) { Py_DECREF(args[i]); } if (res == NULL) { STACK_SHRINK(oparg); goto pop_2_error; } #line 3790 "Python/generated_cases.c.h" STACK_SHRINK(oparg); STACK_SHRINK(1); stack_pointer[-1] = res; next_instr += 3; CHECK_EVAL_BREAKER(); DISPATCH(); }
當(dāng)調(diào)用函數(shù)時(shí),會(huì)執(zhí)行 _PyFunction_Vectorcall,否則執(zhí)行 PyObject_Vectorcall。 以上就是函數(shù)的調(diào)用邏輯,然后再補(bǔ)充一點(diǎn),我們說(shuō) PyFrameObject 是根據(jù) PyCodeObject 創(chuàng)建的,而 PyFunctionObject 也是根據(jù) PyCodeObject 創(chuàng)建的,那么 PyFrameObject 和 PyFunctionObject 之間有啥關(guān)系呢? 如果把 PyCodeObject 比喻成妹子的話,那么 PyFunctionObject 就是妹子的備胎,PyFrameObject 就是妹子的心上人。其實(shí)在棧幀中執(zhí)行指令的時(shí)候,PyFunctionObject 的影響就已經(jīng)消失了。 也就是說(shuō),最終是 PyFrameObject 對(duì)象和 PyCodeObject 對(duì)象兩者如膠似漆,跟 PyFunctionObject 對(duì)象之間沒(méi)有關(guān)系,所以 PyFunctionObject 辛苦一場(chǎng),實(shí)際上是為別人做了嫁衣。PyFunctionObject 主要是對(duì) PyCodeObject 和 global 名字空間的一種打包和運(yùn)輸方式。
|