《深度剖析CPython解釋器》14. Python函數機制的深度解析(第一部分): 函數在底層的數據結構、以及它的創建方式


楔子

函數是任何一門編程語言都具備的基本元素,它可以將多個動作組合起來,一個函數代表了一系列的動作。當然我們之前說函數也是一個變量,該變量指向一個函數。而且在調用函數時會干什么來着,沒錯,要在運行時棧中創建棧幀,用於函數的執行。

那么下面就來看看函數在C中是如何實現的,生得一副什么模樣。

PyFunctionObject對象

我們說過Python中一切皆對象,函數也不例外。在Python中,函數這種抽象機制是通過PyFunctionObject對象實現的,位於 Include/funcobject.h 中。

typedef struct {
    PyObject_HEAD               /* 頭部信息, 不用多說 */
    PyObject *func_code;        /* 函數的PyCodeObject對象, 因為函數就是根據該PyCodeObject對象創建的 */
    PyObject *func_globals;     /* 函數的global名字空間 */
    PyObject *func_defaults;    /* 函數參數的默認值, 一個元組或者空 */
    PyObject *func_kwdefaults;  /* 只能通過關鍵字參數傳遞的"參數"和"該參數的默認值", 一個字典或者空 */
    PyObject *func_closure;     /* 獲取閉包對象 */
    PyObject *func_doc;         /* 函數的doc */
    PyObject *func_name;        /* 函數名 */
    PyObject *func_dict;        /* 屬性字典, 一般為空 */
    PyObject *func_weakreflist; /* 弱引用列表 */
    PyObject *func_module;      /* 函數所在的模塊 */
    PyObject *func_annotations; /* 函數參數的注解, 一個字典或者空 */
    PyObject *func_qualname;    /* 函數的全限定名, 我們后面會說它和func_name之間的區別 */
    vectorcallfunc vectorcall;

    /* Invariant:
     *     func_closure contains the bindings for func_code->co_freevars, so
     *     PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
     *     (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
     */
} PyFunctionObject;

PyFunctionObject的這些成員都是以func開頭的,比如:func_name,但是我們在Python中獲取的時候直接通過__name__獲取即可。

func_code:函數的字節碼

def foo(a, b, c):
    pass


code = foo.__code__
print(code)  # <code object foo at 0x000001D250B9D3A0, file "C:/Users/satori/Desktop/三無少女/2.py", line 1>
print(code.co_varnames)  # ('a', 'b', 'c')

func_globals:global命名空間

def foo(a, b, c):
    pass


name = "夏色祭"
# __globals__其實就是外部的global名字空間
print(foo.__globals__)  # {......, 'name': '夏色祭'}
print(foo.__globals__ == globals())  # True

func_defaults:函數參數的默認值

def foo(name="夏色祭", age=-1):
    pass


# 打印的是默認值
print(foo.__defaults__)  # ('夏色祭', -1)


def bar(): 
    pass

# 沒有默認值的話, __defaults__為None
print(bar.__defaults__)  # None

func_kwdefaults:只能通過關鍵字參數傳遞的"參數"和"該參數的默認值"

def foo(name="夏色祭", age=-1):
    pass


# 打印是為None的, 這是因為雖然有默認值, 但是它並不要求必須通過關鍵字的方式傳遞
print(foo.__kwdefaults__)  # None


# 如果在前面加上一個*, 表示后面的參數就必須通過關鍵字的方式傳遞
# 因為如果不通過關鍵字的話, 那么無論多少個位置參數都會被*給吸收掉, 無論如何也不可能傳遞給name、age
# 我們經常會看到*args, 這是因為我們需要函數調用時傳遞過來的值, 所以可以通過args以元組的形式來拿到這些值
# 但是這里我們不需要, 我們只是希望后面的參數必須通過關鍵字參數傳遞, 因此前面寫一個*即可
# 當然寫*args或者其他的也可以, 但是我們用不到, 所以寫一個*即可
def bar(*, name="夏色祭", age=-1):
    pass


# 此時就打印了默認值,因為這是只能通過kw(關鍵字)傳遞的參數的默認值
print(bar.__kwdefaults__)  # {'name': 'satori', 'age': 16}

func_closure:閉包對象

def foo():
    name = "夏色祭"
    age = -1

    def bar():
        nonlocal name
        nonlocal age

    return bar


# 查看的是閉包里面nonlocal的值
# 這里有兩個nonlocal,所以foo().__closure__是一個有兩個元素的元組
print(foo().__closure__)  # (<cell at 0x000001FD1D3B02B0: int object at 0x00007FFDE559D660>,
                          # <cell at 0x000001FD1D42E310: str object at 0x000001FD1D3DA090>)

print(foo().__closure__[0].cell_contents)  # -1
print(foo().__closure__[1].cell_contents)  # 夏色祭

# 注意:查看閉包屬性我們使用的是內層函數,不是外層的foo

func_doc:函數的文檔

def foo(name, age):
    """
    接收一個name和age,
    返回一句話
    my name is $name, age is $age
    """
    return f"my name is {name}, age is {age}"


print(foo.__doc__)
"""

    接收一個name和age,
    返回一句話
    my name is $name, age is $age
    
"""

func_name:函數名

def foo(name, age):
    pass


print(foo.__name__)  # foo

func_dict:屬性字典

def foo(name, age):
    pass


# 一般函數的屬性字典都會空,屬性字典基本上在類里面使用
print(foo.__dict__)  # {}

func_weakreflist:弱引用列表

Python無法獲取這個屬性,底層沒有提供相應的接口。

func_module:函數所在的模塊

 
def foo(name, age):
    pass


print(foo.__module__)  # __main__

func_annotations:注解

def foo(name: str, age: int):
    pass

# Python3.5的時候新增的語法
print(foo.__annotations__)  # {'name': <class 'str'>, 'age': <class 'int'>}

func_qualname:全限定名

def foo():
    pass


print(foo.__name__, foo.__qualname__)  # foo foo


class A:

    def foo(self):
        pass


print(A.foo.__name__, A.foo.__qualname__)  # foo A.foo

在PyFunctionObject的定義中,我們看到一個func_code成員,指向了一個PyCodeObject對象,我們說函數就是根據這個PyCodeObject對象創建的。因為我們知道一個PyCodeObject對象是對一段代碼的靜態表示,Python編譯器在將源代碼進行編譯之后,對里面的每一個代碼塊(code block)都會生成一個、並且是唯一一個PyCodeObject對象,這個PyCodeObject對象中包含了這個代碼塊中的一些靜態信息,也就是可以從源代碼中看到的信息。

比如:某個函數對應的code block中有一個 name = "夏色祭" 這樣的表達式,那么符號"a"和對應的值1、以及它們之間的聯系就是靜態信息。這些信息會被靜態存儲起來,符號"a"會被存在符號表co_varnames中、值1會被存在常量池co_consts中、這兩者之間是一個賦值,因此會有兩條指令LOAD_CONSTS和STORE_FAST存在字節碼指令序列co_code中。

這些信息是編譯的時候就可以得到的,因此PyCodeObject對象是編譯時候的結果。

但是PyFunctionObject對象是何時產生的呢?實際上PyFunctionObject對象是Python代碼在運行時動態產生的,更准確的說,是在執行一個def語句的時候創建的。

當Python虛擬機在當前棧幀中執行字節碼時發現了def語句,那么就代表發現了新的PyCodeObject對象,因為它們是可以層層嵌套的。所以虛擬機會根據這個PyCodeObject對象創建對應的PyFunctionObject對象,然后將函數名和函數體對應的PyFunctionObject對象組成鍵值對放在當前的local空間中。

顯然在PyFunctionObject對象中,也會包含這些函數的靜態信息,這些信息存儲在func_code中,實際上,func_code一定會指向與函數對應的PyCodeObject對象。除此之外,PyFunctionObject對象中還包含了一些函數在執行時所必須的動態信息,即上下文信息。比如func_globals,就是函數在執行時關聯的global作用域(globals),說白了就是讓你在局部變量找不到的時候能夠找全局變量,可如果連global空間都沒有的話,那即便想找也無從下手呀。而global作用域中的符號和值必須在運行時才能確定,所以這部分必須在運行時動態創建,無法存儲在PyCodeObject中,所以要根據PyCodeObject對象創建PyFunctionObject對象,相當於一個封裝。總之一切的目的,都是為了更好的執行字節碼。

我們舉個栗子:

# 首先虛擬機從上到下執行字節碼
name = "夏色祭"
age = -1


# pia, 出現了一個def
def foo():
    pass


# 那么知道源代碼進入了一個新的作用域了, 這里遇到一個新的PyCodeObject對象了
# 而通過def知道這是一個函數, 所以會進行封裝, 將PyCodeObject對象封裝成PyFunctionObject
# 所以當執行完def語句之后, 一個函數就被創建了, 放在當前的local空間中, 當然對於模塊來說: local空間也是global空間
print(locals())  # {......, 'foo': <function foo at 0x000001B299FAF3A0>}


# 函數的類型是<class 'function'>, 當然這個類Python沒有暴露給我們
# 當我們調用函數foo的時候, 會從local空間中取出符號"foo"對應的PyFunctionObject對象
# 然后根據這個PyFunctionObject對象創建PyFrameObject對象, 也就是為函數創建一個棧幀
# 然后將執行權交給新創建的棧幀, 在新創建的棧幀中執行字節碼

函數對象如何創建

我們現在已經看清了函數的模樣,它在底層對應PyFunctionObject對象,並且它和PyCodeObject對象關系密切。那么Python底層又是如何完成PyCodeObject對象到PyFunctionObject對象之間的轉變呢?想了解這其中的奧秘,就必須要從字節碼入手。

s = """
name = "夏色祭"
def foo(a, b):
    print(a, b)

foo(1, 2)
"""

import dis
dis.dis(compile(s, "func", "exec"))
  2           0 LOAD_CONST               0 ('夏色祭')
              2 STORE_NAME               0 (name)

  3           4 LOAD_CONST               1 (<code object foo at 0x000001EE0CBA72F0, file "func", line 3>)
              6 LOAD_CONST               2 ('foo')
              8 MAKE_FUNCTION            0
             10 STORE_NAME               1 (foo)

  6          12 LOAD_NAME                1 (foo)
             14 LOAD_CONST               3 (1)
             16 LOAD_CONST               4 (2)
             18 CALL_FUNCTION            2
             20 POP_TOP
             22 LOAD_CONST               5 (None)
             24 RETURN_VALUE

Disassembly of <code object foo at 0x000001EE0CBA72F0, file "func", line 3>:
  4           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (a)
              4 LOAD_FAST                1 (b)
              6 CALL_FUNCTION            2
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

顯然這個代碼中出現了兩個PyCodeObject對象,一個對應整個py文件,另一個則是對應函數foo。

s = """
name = "夏色祭"
def foo(a, b):
    print(a, b)

foo(1, 2)
"""

# 把字符串當成是一個py文件來進行編譯
co = compile(s, "func", "exec")

print(co.co_consts)  
# ('夏色祭', <code object foo at 0x00000183F9101450, file "func", line 3>, 'foo', 1, 2, None)

print(co.co_name)  # <module>
print(co.co_consts[1].co_name)  # foo

可以看到,"函數foo對應的PyCodeObject對象"是"py文件對應的PyCodeObject對象"的常量池co_consts中的一個元素。因為在對py文件創建PyCodeObject對象的時候,發現了一個函數代碼塊foo,那么會對函數代碼塊foo繼續創建一個PyCodeObject對象(每一個代碼塊都會對應一個PyCodeObject對象),而函數foo對應的PyCodeObject對象則是py文件對應的PyCodeObject對象的co_consts常量池當中的一個元素。

通過以上例子,我們發現PyCodeObject對象是嵌套的。之前我們我們說過,每一個code block(函數、類等等)都會對應一個PyCodeObject對象。現在我們又看到了,根據層級來分的話,"內層代碼塊對應的PyCodeObject對象"是"最近的外層代碼塊對應的PyCodeObject對象"的常量池co_consts中的一個元素。而最外層則是模塊對應的PyCodeObject對象,因此這就意味着我們通過最外層的PyCodeObject對象可以找到所有的PyCodeObject對象,顯然這是毋庸置疑的。而這里和棧幀也是對應的,棧幀我們說過也是層層嵌套的,而內層棧幀通過f_back可以找到外層、也就是調用者對應的棧幀,當然這里我們之前的章節已經說過了,這里再提一遍。

這里再來重新看一下上面的字節碼:

  2           0 LOAD_CONST               0 ('夏色祭')
              2 STORE_NAME               0 (name)

  3           4 LOAD_CONST               1 (<code object foo at 0x000001EE0CBA72F0, file "func", line 3>)
              6 LOAD_CONST               2 ('foo')
              8 MAKE_FUNCTION            0
             10 STORE_NAME               1 (foo)

  6          12 LOAD_NAME                1 (foo)
             14 LOAD_CONST               3 (1)
             16 LOAD_CONST               4 (2)
             18 CALL_FUNCTION            2
             20 POP_TOP
             22 LOAD_CONST               5 (None)
             24 RETURN_VALUE

Disassembly of <code object foo at 0x000001EE0CBA72F0, file "func", line 3>:
  4           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (a)
              4 LOAD_FAST                1 (b)
              6 CALL_FUNCTION            2
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE

顯然dis模塊自動幫我們分成了兩部分,上面是模塊的字節碼,下面是函數的字節碼。首先函數很簡單我們就不看了,直接看模塊的。

首先開頭的LOAD_CONST和STORE_NAME顯然是 name = "夏色祭" 對應的指令。然后我們看4 LOAD_CONST,這條指令也是加載了一個常量,但這個常量是一個PyCodeObject對象;6 LOAD_CONST則是將字符串常量"foo"、即函數名加載了進來,然后通過MAKE_FUNCTION指令構建一個PyFunctionObject對象;然后10 STORE_NAME,讓符號foo指向這個PyFunctionObject對象。再下面就是函數調用了,函數調用的具體細節我們之后會詳細說。

並且我們還看到一個有趣的現象,那就是源代碼的行號。我們發現之前看到源代碼的行號都是從上往下、依次增大的,這很好理解,畢竟一條一條解釋嘛。但是這里卻發生了變化,先執行了第6行,之后再執行第4行。如果是從Python層面的函數調用來理解的話,很容易一句話就解釋了,因為函數只有在調用的時候才會執行。但是從字節碼的角度來理解的話,我們發現函數的聲明和實現是分離的,是在不同的PyCodeObject對象中。確實如此,雖然一個函數名和函數體是一個整體,但是Python虛擬機在實現這個函數的時候,卻在物理上將它們分離開了,構建函數的字節碼指令序列必須在模塊對應的PyCodeObject對象中。

我們之前說過,函數即變量。我們是可以把函數當成是普通的變量來處理的,函數名就相當於變量名,函數體就相當於是變量指向的值。而foo函數顯然是在全局中定義的一個函數,那么foo是不是要出現在py文件對應的PyCodeObject對象的符號表co_names里面呢?foo對應的PyCodeObject對象是不是要出現在py文件對應的PyCodeObject對象的常量池co_consts里面呢?

至此,函數的結構就已經非常清晰了。

所以函數名和函數體是分離的,它們存在不同的PyCodeObject對象當中。分析完結構之后,我們的重點就在於那個MAKE_FUNCTION指令了,我們說當遇到def foo(a, b)的時候,在語法上將這是函數的聲明語句,但是從虛擬機的角度來看這其實是函數對象的創建語句。所以下面我們就要分析一下這個指令,看看它到底是怎么將一個PyCodeObject對象變成一個PyFunctionObject對象的。

        case TARGET(MAKE_FUNCTION): {
            PyObject *qualname = POP(); //彈出符號表中的函數名
            PyObject *codeobj = POP();  //彈出對應的字節碼對象
            //創建PyFunctionObject對象, 接收三個參數, 首先第一個參數和第三個參數很好理解, 但重點是第二個參數
            //首先f指的就是當前所在的棧幀, 對於我們目前這個里而言就是模塊、或者py文件對應的棧幀
            //然后將f_globals、也就是global名字空間傳遞了進去, 所以我們現在明白了為什么函數可以調用__globals__了
            //當然也明白為什么函數可以在局部變量找不到的時候去找全局變量了
            PyFunctionObject *func = (PyFunctionObject *)
                PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);

            Py_DECREF(codeobj);
            Py_DECREF(qualname);
            if (func == NULL) {
                goto error;
            }
			
            //下面是設置閉包、注解、參數默認值等屬性
            if (oparg & 0x08) {
                assert(PyTuple_CheckExact(TOP()));
                func ->func_closure = POP();
            }
            if (oparg & 0x04) {
                assert(PyDict_CheckExact(TOP()));
                func->func_annotations = POP();
            }
            if (oparg & 0x02) {
                assert(PyDict_CheckExact(TOP()));
                func->func_kwdefaults = POP();
            }
            if (oparg & 0x01) {
                assert(PyTuple_CheckExact(TOP()));
                func->func_defaults = POP();
            }
			
            //將函數或者說函數對象壓入運行時棧
            PUSH((PyObject *)func);
            DISPATCH();
        }

我們看到在MAKE FUNCTION之前,先進行了LOAD CONST,顯然是將foo對應的字節碼對象和符號foo壓入到了棧中。所以在執行MAKE FUNCTION的時候,首先就是將這個字節碼對象以及對應符號彈出棧,然后再加上當前PyFrameObject對象中維護的global名字空間f_globals對象,三者作為參數傳入PyFunction_NewWithQualName函數中,從而構建出相應的PyFunctionObject對象。

下面我們來看看PyFunction_NewWithQualName是如何構造出一個函數的,它位於 Objects/funcobject.c 中。

PyObject *
PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname)
{	
    //要返回的PyFunctionObject *, 這里先聲明一下
    PyFunctionObject *op;
    //函數的doc、PyCodeObject的co_consts、函數所在的模塊
    PyObject *doc, *consts, *module;
    static PyObject *__name__ = NULL;

    if (__name__ == NULL) {
        __name__ = PyUnicode_InternFromString("__name__");
        if (__name__ == NULL)
            return NULL;
    }
	
    //通過PyObject_GC_New為函數對象申請空間
    op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type);
    if (op == NULL)
        return NULL;
	
    //下面就是設置PyFunctionObject對象的成員屬性了
    op->func_weakreflist = NULL;
    Py_INCREF(code);
    op->func_code = code;
    Py_INCREF(globals);
    op->func_globals = globals;
    op->func_name = ((PyCodeObject *)code)->co_name;
    Py_INCREF(op->func_name);
    op->func_defaults = NULL; /* No default arguments */
    op->func_kwdefaults = NULL; /* No keyword only defaults */
    op->func_closure = NULL;
    op->vectorcall = _PyFunction_Vectorcall;
	
    //通過PyCodeObject對象獲取常量池
    consts = ((PyCodeObject *)code)->co_consts;
    
    //我們知道函數的doc其實就是一個字符串, 顯然它也是常量池的一個常量, 並且是常量池的第一個元素
    //否則的話它就是不能成為doc
    if (PyTuple_Size(consts) >= 1) {
        //所以如果consts>=1, 並且第一個元素是字符串, 那么它就是函數的doc
        doc = PyTuple_GetItem(consts, 0);
        if (!PyUnicode_Check(doc))
            doc = Py_None;
    }
    else //否則doc就是None
        doc = Py_None;
    Py_INCREF(doc);
    //下面也是設置PyFunctionObject對象的成員
    op->func_doc = doc;

    op->func_dict = NULL;
    op->func_module = NULL;
    op->func_annotations = NULL;

    /* __module__: If module name is in globals, use it.
       Otherwise, use None. */
    module = PyDict_GetItemWithError(globals, __name__);
    if (module) {
        Py_INCREF(module);
        op->func_module = module;
    }
    else if (PyErr_Occurred()) {
        Py_DECREF(op);
        return NULL;
    }
    if (qualname)
        op->func_qualname = qualname;
    else
        op->func_qualname = op->func_name;
    Py_INCREF(op->func_qualname);

    _PyObject_GC_TRACK(op);
    return (PyObject *)op;
}

所以通過MAKE_FUNCTION我們便創建了PyFunctionObject對象,然后它會被壓入棧中,再通過STORE_NAME將符號foo和PyFunctionObject對象組成一個entry,存儲在當前棧幀的local名字空間中,當然也是global名字空間。只不過為了和函數保持統一,我們都說成local名字空間,只不過不同的作用域對應的local空間是不一樣的。

當然了我們說函數對象的類型是<class 'function'>,但是這個類底層沒有暴露給我們,但是我們依舊可以通過曲線救國的方式進行獲取。

def f():
    pass

print(type(f))  # <class 'function'>
# lambda匿名函數的類型也是<class 'function'>
print(type(lambda: None))  # <class 'function'>

所以我們可以仿照底層的思路,通過<class 'function'>來創建一個函數對象。

gender = "female"


def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"


# 得到PyCodeObject對象
code = f.__code__
# 根據class function創建函數對象, 接收三個參數: PyCodeObject對象、名字空間、函數名
new_f = type(f)(code, globals(), "根據f創建的new_f")

# 打印函數名
print(new_f.__name__)  # 根據f創建的new_f

# 調用函數
print(new_f("夏色祭", -1))  # name: 夏色祭, age: -1, gender: female

是不是很神奇呢?另外我們說函數在訪問gender指向的對象時,顯然先從自身的符號表中找,如果沒有那么回去找全局變量。這是因為,我們在創建函數的時候將global名字空間傳進去了,如果我們不傳遞呢?

gender = "female"


def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"


code = f.__code__
try:
    new_f = type(f)(code, None, "根據f創建的new_f")
except TypeError as e:
    print(e)  # function() argument 'globals' must be dict, not None
# 這里告訴我們function的第二個參數globals必須是一個字典
# 我們傳遞一個空字典
new_f1 = type(f)(code, {}, "根據f創建的new_f1")

# 打印函數名
print(new_f1.__name__)  # 根據f創建的new_f1

# 調用函數
try:
    print(new_f1("夏色祭", -1))
except NameError as e:
    print(e)  # name 'gender' is not defined

# 我們看到告訴我們gender沒有定義

因此現在我們又在Python的角度上理解了一遍,為什么Python中的函數能夠在局部變量找不到的時候,去找全局變量,原因就在於構建函數的時候,將global名字空間交給了函數。使得函數可以在global空間進行變量查找,所以它才能夠找到全局變量。而我們這里給了一個空字典,那么顯然就找不到gender這個變量了。

gender = "female"


def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"


code = f.__code__
new_f = type(f)(code, {"gender": "萌妹子"}, "根據f創建的new_f")

# 我們可以手動傳遞一個字典進去, 此時我們傳遞的字典對於函數來說就是global名字空間
# 所以在函數內部找不到某個變量的時候, 就會去我們指定的名字空間中找
print(new_f("夏色祭", -1))  # name: 夏色祭, age: -1, gender: 萌妹子
# 所以此時的gender不再是外部的"female", 而是我們指定的"萌妹子"

此外我們還可以為函數指定默認值:

gender = "female"


def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"


code = f.__code__
new_f = type(f)(code, {"gender": "屑女仆"}, "根據f創建的new_f")

# 必須接收一個PyTupleObject對象
new_f.__defaults__ = ("神樂mea", 38)
# 即使我們不傳遞參數, 也是完全可以的, 因為已經有默認值了
print(new_f())  # name: 神樂mea, age: 38, gender: 屑女仆


# 我們也可以指定部分默認參數
new_f1 = type(f)(code, {"gender": "屑女仆"}, "根據f創建的new_f1")
# 這里的在設置默認值的時候是從后往前設置的, 比如: ("神樂mea", 38)
# 是將38設置為age的默認值, "神樂mea"設置為name的默認值
# 所以這里的(38,) , 會將38設置為age的默認值, 不是name
# 那name怎么辦? 如果沒有對應的默認值了, 那么它就必須在函數調用的時候由我們顯式的傳遞
new_f1.__defaults__ = (38,)
try:
    new_f1()
except TypeError as e:
    print(e)  # f() missing 1 required positional argument: 'name'

print(new_f1("神楽めあ"))  # name: 神楽めあ, age: 38, gender: 屑女仆

"""
但是問題來了, 為什么在設置默認值的時候要從后往前呢?
首先如果默認值的個數和參數的個數正好匹配, 那么相安無事, 如果不匹配那么只能是默認值的個數小於參數個數
如果是從后往前, 那么(38,)就意味着38設置為age的默認值, name就必須由我們在調用的時候傳遞
但如果是從前往后, 那么(38,)就意味着38設置為name的默認值, age就必須由我們在調用的時候來傳遞

但是問題來了, 如果38設置為name的默認值, 這會是什么情況? 顯然等價於:
def new_f1(name=38, age):
    ......
    
你認為這樣的函數能夠通過編譯嗎?顯然是不行的, 因為默認參數必須在非默認參數的后面
"""
# 所以Python的這個做法是完全正確的, 必須要從后往前進行設置

當然,這種設置默認值的方式顯然也可以使用於通過def定義的函數,因為我們上面的new_f、new_f1和f都是<class 'function'>對象。

gender = "female"


def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"


print(f.__defaults__)  # None

# 設置默認值
f.__defaults__ = ("夏色祭", -1)

# 如果你用的是pycharm, 那么會在f()這個位置給你做上標記, 提示你參數沒有傳遞
# 但我們知道由於使用__defaults__已經設置了默認值, 所以這里是不會報錯的, 只不過pycharm沒有檢測到, 當然基本上所有的ide都無法做到這一點
print(f())  # name: 夏色祭, age: -1, gender: female

另外我們說,默認值的個數一定要小於等於參數的個數,但如果大於呢?

gender = "female"


def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"


print(f.__defaults__)  # None

f.__defaults__ = ("夏色祭", -1, "神樂mea", 38)

print(f())  # name: 神樂mea, age: 38, gender: female

# 依舊從后往前, 38給age、"神樂mea"給name
# 參數都有默認值了, 那么就結束了
# 當然如果是__defaults__指向的元組先結束, 那么沒有得到默認值的參數就必須由我們來傳遞了

想不到Python中的函數可以玩出這么多新花樣,現在你是不是對函數有了一個更深刻的認識了呢?當然目前介紹的只是函數的一小部分內容,還有函數如何調用、位置參數和關鍵字參數如何解析、對於有默認值的參數如何在我們不傳遞的時候使用默認值以及在我們傳遞的時候使用我們傳遞的值、*args和**kwargs又如何解析、閉包怎么做到的、還有裝飾器等等等等,這些我們接下來會單獨用幾篇博客詳細說。

因為放在一篇博客里面的話,字數至少要好幾萬,而我使用的Markdown編輯器typora在字數達到一萬五的時候就會出現明顯卡頓,要是一下子都寫完的話,絕對卡到爆,而且越往后越卡,這對我而言也是個痛苦。而且函數的內容也比較多,我們就多用一些篇幅去介紹它吧。

判斷函數都有哪些參數

最后我們再來看看我們如何檢測一個函數有哪些參數,首先函數的局部變量(包括參數)在編譯是就已經確定,會存在符號表co_varnames中。

# 注意: 在定義函數的時候*和**最多只能出現一次
# 顯然a和b必須通過位置參數傳遞
# c和d可以通過位置參數或者關鍵字參數傳遞
# e和f必須通過關鍵字參數傳遞
def f(a, b, /, c, d, *args, e, f, **kwargs):
    g = 1
    h = 2


varnames = f.__code__.co_varnames
print(varnames)
# ('a', 'b', 'c', 'd', 'e', 'f', 'args', 'kwargs', 'g', 'h')

"""
首先co_varnames打印的符號表是有順序的, 參數永遠在函數內部定義的局部變量的前面
g和h就是函數內部定義的局部變量, 所以它在所有的后面

如果是參數的話, 那么*和**會位於最后面, 其它參數位置不變, 所以除了g和h, 最后面的就是args和kwargs
"""

# 接下來, 我們就可以進行判斷了
# 1. 尋找必須通過位置參數傳遞的參數
posonlyargcount = f.__code__.co_posonlyargcount
print(posonlyargcount)  # 2
print(varnames[: posonlyargcount])  # ('a', 'b')

# 2. 尋找可以通過位置參數傳遞或者關鍵字參數傳遞的參數
argcount = f.__code__.co_argcount
print(argcount)  # 4
print(varnames[: 4])  # ('a', 'b', 'c', 'd')
print(varnames[posonlyargcount: 4])  # ('c', 'd')

# 3. 尋找必須通過關鍵字參數傳遞的參數
kwonlyargcount = f.__code__.co_kwonlyargcount
print(kwonlyargcount)  # 2
print(varnames[argcount: argcount + kwonlyargcount])  # ('e', 'f')

# 4. 尋找*args和**kwargs
"""
在介紹PyCodeObject對象的時候, 我們說里面有一個co_flags成員
它是專門用來判斷參數中是否有*args和**kwargs的
"""
flags = f.__code__.co_flags
# 如果flags和4進行按位與之后為真, 那么就代表有*args, 否則沒有
# 如果flags和8進行按位與之后為真, 那么就代表有**kwargs, 否則沒有
step = argcount + kwonlyargcount
if flags & 0x04:
    print(varnames[step])  # args
    step += 1

if flags & 0x08:
    print(varnames[step])  # kwargs
    
# 雖然我們這里打印的是args和kwargs, 但主要取決定義的時候使用的名字
# 如果定義的時候是*ARGS和**KWARGS, 那么這里就是ARGS和KWARGS, 只不過一般我們都叫做*args和**kwargs

如果我們定義的不是*args,只是一個*,那么它就不是參數了。

def f(a, b, *, c):
    pass


# 我們看到此時只有a、b、c
print(f.__code__.co_varnames)  # ('a', 'b', 'c')

print(f.__code__.co_flags & 0x04)  # 0
print(f.__code__.co_flags & 0x08)  # 0
# 顯然此時也都為假

小結

這一次我們簡單的分析了一下函數在底層對應的數據結構,以及如何創建一個函數,並且還在Python的層面上做了一些小trick。最后我們也分析了如何通過PyCodeObject對象來檢索Python中的參數,以及相關種類,當然標准庫中的inspect模塊也是這么做的。當然說白了,其實是我們模仿人家的思路做的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM