久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

函數(shù)在底層是如何調(diào)用的,?

 古明地覺O_o 2024-11-19 發(fā)布于北京

楔子


上一篇文章我們說了 Python 函數(shù)的底層實現(xiàn),并且還演示了如何通過函數(shù)的類型對象自定義一個函數(shù),,以及如何獲取函數(shù)的參數(shù),。雖然這在工作中沒有太大意義,但是可以讓我們深刻理解函數(shù)的行為,。

那么接下來看看函數(shù)是如何調(diào)用的,。


PyCFunctionObject


在介紹調(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í)行方式。


函數(shù)的調(diào)用


我們來調(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)輸方式。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多