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