午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

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

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

楔子


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

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


PyCFunctionObject


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


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


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

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

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多