前面我們介紹了函數(shù)的基本結(jié)構(gòu),它在底層由 PyFunctionObject 結(jié)構(gòu)體表示,那么本篇文章來(lái)看看函數(shù)的創(chuàng)建過(guò)程。 正好明天周六日,可以慢慢看[doge]。 函數(shù)是何時(shí)創(chuàng)建的 介紹函數(shù)結(jié)構(gòu)時(shí),我們看到內(nèi)部有一個(gè) func_code 字段,指向一個(gè) PyCodeObject 對(duì)象,而函數(shù)就是根據(jù) PyCodeObject 對(duì)象創(chuàng)建的。 因?yàn)橐粋€(gè) PyCodeObject 是對(duì)一段代碼的靜態(tài)表示,Python 編譯器將源代碼編譯之后,針對(duì)里面的每一個(gè)代碼塊(code block)都會(huì)生成相應(yīng)的 PyCodeObject 對(duì)象,該對(duì)象包含了這個(gè)代碼塊的一些靜態(tài)信息,也就是可以從源代碼中看到的信息。 比如某個(gè)函數(shù)對(duì)應(yīng)的代碼塊里面有一個(gè) a = 1 這樣的表達(dá)式,那么符號(hào) a 和整數(shù) 1、以及它們之間的聯(lián)系就是靜態(tài)信息。這些信息會(huì)被靜態(tài)存儲(chǔ)起來(lái),符號(hào) a 被存在符號(hào)表 co_varnames 中,整數(shù) 1 被存在常量池 co_consts 中。然后 a = 1 是一條賦值語(yǔ)句,因此會(huì)有兩條指令 LOAD_CONST 和 STORE_FAST 存在字節(jié)碼指令序列 co_code 中。 這些信息是在編譯的時(shí)候就可以得到的,因此 PyCodeObject 對(duì)象是編譯之后的結(jié)果。 但 PyFunctionObject 對(duì)象是何時(shí)產(chǎn)生的呢?顯然它是 Python 代碼在運(yùn)行時(shí)動(dòng)態(tài)產(chǎn)生的,更準(zhǔn)確的說(shuō),是在執(zhí)行一個(gè) def 語(yǔ)句的時(shí)候創(chuàng)建的。當(dāng)虛擬機(jī)發(fā)現(xiàn)了 def 語(yǔ)句,那么就代表發(fā)現(xiàn)了新的 PyCodeObject 對(duì)象,因?yàn)樗鼈兪强梢詫訉忧短椎摹?/span> 然后虛擬機(jī)會(huì)根據(jù)這個(gè) PyCodeObject 對(duì)象創(chuàng)建對(duì)應(yīng)的 PyFunctionObject 對(duì)象,并將變量名和 PyFunctionObject 對(duì)象(函數(shù)體)組成鍵值對(duì)放在當(dāng)前的 local 空間中。 而在 PyFunctionObject 對(duì)象中,也需要拿到相關(guān)的靜態(tài)信息,因此會(huì)有一個(gè) func_code 字段指向 PyCodeObject。 除此之外,PyFunctionObject 對(duì)象還包含了一些函數(shù)在執(zhí)行時(shí)所必需的動(dòng)態(tài)信息,即上下文信息。比如 func_globals,就是函數(shù)在執(zhí)行時(shí)關(guān)聯(lián)的 global 名字空間,如果沒(méi)有這個(gè)空間的話,函數(shù)就無(wú)法訪問(wèn)全局變量了。 由于 global 作用域中的符號(hào)和值必須在運(yùn)行時(shí)才能確定,所以這部分必須在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建,無(wú)法靜態(tài)存儲(chǔ)在 PyCodeObject 中。因此要基于 PyCodeObject 對(duì)象和 global 名字空間來(lái)創(chuàng)建 PyFunctionObject 對(duì)象,相當(dāng)于一個(gè)封裝??傊磺械哪康?,都是為了更好地執(zhí)行字節(jié)碼。 我們舉個(gè)例子: # 首先虛擬機(jī)從上到下執(zhí)行字節(jié)碼 name = "古明地覺(jué)" age = 17
# 啪,很快啊,出現(xiàn)了一個(gè) def def foo(): pass
# 出現(xiàn)了 def,虛擬機(jī)就知道源代碼進(jìn)入了一個(gè)新的作用域了 # 也就是遇到一個(gè)新的 PyCodeObject 對(duì)象了 # 而通過(guò) def 關(guān)鍵字知道這是一個(gè)函數(shù),于是會(huì)進(jìn)行封裝 # 將 PyCodeObject 封裝成 PyFunctionObject,同時(shí)包含了全局名字空間 # 所以當(dāng)執(zhí)行完 def 語(yǔ)句之后,一個(gè)函數(shù)就被創(chuàng)建了 # 然后將變量名 foo 和函數(shù)體(PyFunctionObject)組成鍵值對(duì)存放在當(dāng)前的 local 空間中 # 當(dāng)然對(duì)于模塊而言,local 空間也是 global 空間 print({k: v for k, v in locals().items() if k == "foo"}) """ {'foo': <function foo at 0x102d5bf40>} """
# 函數(shù)內(nèi)部也保存了 global 空間 print(foo.__globals__ is globals() is locals()) """ True """ print(foo.__globals__["foo"] is foo is locals()["foo"]) """ True """
調(diào)用的時(shí)候,會(huì)從 local 空間中取出符號(hào) foo 對(duì)應(yīng)的 PyFunctionObject 對(duì)象(函數(shù)對(duì)象)。然后根據(jù)函數(shù)對(duì)象創(chuàng)建棧幀對(duì)象,也就是為函數(shù)創(chuàng)建一個(gè)棧幀,隨后將執(zhí)行權(quán)交給新創(chuàng)建的棧幀,并在新創(chuàng)建的棧幀中執(zhí)行字節(jié)碼。 經(jīng)過(guò)分析我們知道,當(dāng)執(zhí)行到 def 語(yǔ)句時(shí)會(huì)創(chuàng)建函數(shù),并保存在 local 空間中。而通過(guò)函數(shù)名()進(jìn)行調(diào)用時(shí),會(huì)從 local 空間取出和函數(shù)名綁定的函數(shù)對(duì)象,然后執(zhí)行。 那么問(wèn)題來(lái)了,函數(shù)(對(duì)象)是怎么創(chuàng)建的呢?或者說(shuō)虛擬機(jī)是如何完成 PyCodeObject 對(duì)象到 PyFunctionObject 對(duì)象之間的轉(zhuǎn)變呢?顯然想了解這其中的奧秘,就必須從字節(jié)碼入手。 import dis
code_string = """ name = "satori" def foo(a, b): print(a, b)
foo(1, 2) """
dis.dis(compile(code_string, "<func>", "exec"))
源代碼很簡(jiǎn)單,定義一個(gè)變量 name 和一個(gè)函數(shù) foo,然后調(diào)用函數(shù)。顯然這里面會(huì)產(chǎn)生兩個(gè) PyCodeObject,我們來(lái)看一下。 0 RESUME 0
# name = "satori" 2 LOAD_CONST 0 ('satori') 4 STORE_NAME 0 (name)
# 我們看到 PyCodeObject 也會(huì)作為常量被靜態(tài)收集 # 這里是將常量池中索引為 1 的 PyCodeObject 壓入運(yùn)行時(shí)棧 6 LOAD_CONST 1 (<code object foo at 0x7f5d...>) # 從棧中彈出 PyCodeObject,然后構(gòu)建函數(shù)對(duì)象 # 將函數(shù)對(duì)象(的指針)再壓入運(yùn)行時(shí)棧 8 MAKE_FUNCTION 0 # 從棧中彈出函數(shù)對(duì)象,并用符號(hào) foo 綁定起來(lái) # 到此函數(shù)就創(chuàng)建完畢了 10 STORE_NAME 1 (foo) # 以下是 foo(1, 2) 對(duì)應(yīng)的字節(jié)碼 12 PUSH_NULL # 加載全局變量 foo 并壓棧 14 LOAD_NAME 1 (foo) # 加載常量 1 和 2 并壓棧 16 LOAD_CONST 2 (1) 18 LOAD_CONST 3 (2) # 從棧中彈出函數(shù)和參數(shù),然后調(diào)用 # 將調(diào)用結(jié)果、即函數(shù)的返回值壓入棧中 20 CALL 2 # 從棧頂彈出返回值,因?yàn)槲覀儧](méi)有使用變量保存,所以會(huì)直接丟棄 # 如果使用變量保存了,比如 res = foo(1, 2),那么這里的字節(jié)碼就是 STORE_NAME 28 POP_TOP 30 RETURN_CONST 4 (None) # 以上是模塊對(duì)應(yīng)的字節(jié)碼指令,下面是函數(shù)對(duì)應(yīng)的字節(jié)碼指令 Disassembly of <code object foo at 0x7f5d..., file "<func>", line 3>: 0 RESUME 0 # 比較簡(jiǎn)單,就是 print(a, b) 對(duì)應(yīng)的字節(jié)碼 2 LOAD_GLOBAL 1 (NULL + print) 12 LOAD_FAST 0 (a) 14 LOAD_FAST 1 (b) 16 CALL 2 24 POP_TOP 26 RETURN_CONST 0 (None)
通過(guò)字節(jié)碼我們看到,def 關(guān)鍵字實(shí)際上還是在定義變量,正所謂函數(shù)即變量,我們可以把函數(shù)當(dāng)成普通的變量來(lái)處理。函數(shù)名就是變量名,它位于模塊對(duì)應(yīng)的 PyCodeObject 的符號(hào)表中。函數(shù)體就是變量指向的值,它是基于一個(gè)獨(dú)立的 PyCodeObject 構(gòu)建的。 至此,函數(shù)的結(jié)構(gòu)就已經(jīng)非常清晰了。 分析完結(jié)構(gòu)之后,重點(diǎn)就要落在 MAKE_FUNCTION 指令上了,我們說(shuō)當(dāng)遇到 def 關(guān)鍵字的時(shí)候,就知道要?jiǎng)?chuàng)建函數(shù)了。在語(yǔ)法上這是函數(shù)的聲明語(yǔ)句,但從虛擬機(jī)的角度來(lái)看,這其實(shí)是函數(shù)對(duì)象的創(chuàng)建語(yǔ)句。 所以函數(shù)是怎么創(chuàng)建的,就是執(zhí)行 MAKE_FUNCTION 指令創(chuàng)建的,該指令執(zhí)行完畢后,一個(gè)函數(shù)對(duì)象就被壓入了運(yùn)行時(shí)棧。等到 STORE_NAME 執(zhí)行時(shí),再將它從棧中彈出,然后和變量(函數(shù)名)綁定起來(lái)。 下面我們就來(lái)分析一下 MAKE_FUNCTION 指令,看看它是怎么將一個(gè) PyCodeObject 對(duì)象變成一個(gè) PyFunctionObject 對(duì)象的。 TARGET(MAKE_FUNCTION) { // 獲取 PyCodeObject 對(duì)象 PyObject *codeobj = stack_pointer[-1];
// 編譯時(shí),解釋器能夠靜態(tài)檢測(cè)出函數(shù)有沒(méi)有閉包變量、類型注解等屬性,并體現(xiàn)在 oparg 中 // 構(gòu)建函數(shù)時(shí),通過(guò) oparg 和一系列標(biāo)志位做按位與,來(lái)判斷函數(shù)是否包含指定屬性 // 由于 oparg 是指令參數(shù),所以這些屬性是否存在、以及如何訪問(wèn),在編譯階段就已經(jīng)確定了 PyObject *closure = (oparg & 0x08) ? stack_pointer[...] : NULL; PyObject *annotations = (oparg & 0x04) ? stack_pointer[...] : NULL; PyObject *kwdefaults = (oparg & 0x02) ? stack_pointer[...] : NULL; PyObject *defaults = (oparg & 0x01) ? stack_pointer[...] : NULL; PyObject *func; #line 3267 "Python/bytecodes.c" // 基于 PyCodeObject 和全局名字空間,來(lái)構(gòu)建 PyFunctionObject PyFunctionObject *func_obj = (PyFunctionObject *) PyFunction_New(codeobj, GLOBALS());
Py_DECREF(codeobj); if (func_obj == NULL) { goto error; } // 設(shè)置閉包變量、類型注解、默認(rèn)值等屬性 if (oparg & 0x08) { assert(PyTuple_CheckExact(closure)); func_obj->func_closure = closure; } if (oparg & 0x04) { assert(PyTuple_CheckExact(annotations)); func_obj->func_annotations = annotations; } if (oparg & 0x02) { assert(PyDict_CheckExact(kwdefaults)); func_obj->func_kwdefaults = kwdefaults; } if (oparg & 0x01) { assert(PyTuple_CheckExact(defaults)); func_obj->func_defaults = defaults; }
func_obj->func_version = ((PyCodeObject *)codeobj)->co_version; // 函數(shù)創(chuàng)建之后,將棧里的元素彈出,然后將函數(shù)對(duì)象壓入棧中 func = (PyObject *)func_obj; #line 4534 "Python/generated_cases.c.h" STACK_SHRINK(((oparg & 0x01) ? 1 : 0) + ((oparg & 0x02) ? 1 : 0) + ((oparg & 0x04) ? 1 : 0) + ((oparg & 0x08) ? 1 : 0)); stack_pointer[-1] = func; DISPATCH(); }
整個(gè)步驟很好理解,然后創(chuàng)建函數(shù)體用的是 PyFunction_New,看一下它的邏輯。 // Objects/funcobject.c PyObject * PyFunction_New(PyObject *code, PyObject *globals) { return PyFunction_NewWithQualName(code, globals, NULL); }
PyObject * PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname) { assert(globals != NULL); assert(PyDict_Check(globals)); // 給全局名字空間增加引用計(jì)數(shù) Py_INCREF(globals); // 獲取線程狀態(tài)對(duì)象 PyThreadState *tstate = _PyThreadState_GET(); // 給 PyCodeObject 對(duì)象增加引用計(jì)數(shù) PyCodeObject *code_obj = (PyCodeObject *)Py_NewRef(code);
assert(code_obj->co_name != NULL); // 獲取 co_name 并增加引用計(jì)數(shù) PyObject *name = Py_NewRef(code_obj->co_name); // 獲取 co_qualname,如果存在,增加引用計(jì)數(shù) if (!qualname) { qualname = code_obj->co_qualname; } assert(qualname != NULL); Py_INCREF(qualname); // 獲取常量池 PyObject *consts = code_obj->co_consts; assert(PyTuple_Check(consts)); // 函數(shù)的 docstring 也會(huì)被收集到常量池中,并且是常量池的第一個(gè)元素 // 如果函數(shù)沒(méi)有 docstring,那么常量池的第一個(gè)元素會(huì)是 None PyObject *doc; if (PyTuple_Size(consts) >= 1) { doc = PyTuple_GetItem(consts, 0); // 如果第一個(gè)元素不是字符串,則說(shuō)明函數(shù)沒(méi)有 docstring if (!PyUnicode_Check(doc)) { doc = Py_None; } } else { doc = Py_None; } Py_INCREF(doc);
// 獲取 __module__,并增加引用計(jì)數(shù) PyObject *module = PyDict_GetItemWithError( globals, &_Py_ID(__name__)); PyObject *builtins = NULL; if (module == NULL && _PyErr_Occurred(tstate)) { goto error; } Py_XINCREF(module); // 獲取 __builtins__,并增加引用計(jì)數(shù) builtins = _PyEval_BuiltinsFromGlobals(tstate, globals); if (builtins == NULL) { goto error; } Py_INCREF(builtins); // 為函數(shù)對(duì)象申請(qǐng)內(nèi)存空間 PyFunctionObject *op = PyObject_GC_New( PyFunctionObject, &PyFunction_Type); if (op == NULL) { goto error; } // 初始化函數(shù)對(duì)象的內(nèi)部屬性 op->func_globals = globals; op->func_builtins = builtins; op->func_name = name; op->func_qualname = qualname; op->func_code = (PyObject*)code_obj; op->func_defaults = NULL; op->func_kwdefaults = NULL; op->func_closure = NULL; op->func_doc = doc; op->func_dict = NULL; op->func_weakreflist = NULL; op->func_module = module; op->func_annotations = NULL; op->func_typeparams = NULL; op->vectorcall = _PyFunction_Vectorcall; op->func_version = 0; _PyObject_GC_TRACK(op); handle_func_event(PyFunction_EVENT_CREATE, op, NULL); return (PyObject *)op;
error: Py_DECREF(globals); Py_DECREF(code_obj); Py_DECREF(name); Py_DECREF(qualname); Py_DECREF(doc); Py_XDECREF(module); Py_XDECREF(builtins); return NULL; }
以上就是函數(shù)對(duì)象的創(chuàng)建過(guò)程,說(shuō)白了就是對(duì) PyCodeObject 進(jìn)行了一個(gè)封裝。等函數(shù)對(duì)象創(chuàng)建完畢后會(huì)回到 MAKE_FUNCTION,然后設(shè)置閉包、注解等屬性,并將函數(shù)對(duì)象壓入棧中。接著執(zhí)行 STORE_NAME 從符號(hào)表中加載符號(hào)(函數(shù)名),并從棧頂彈出函數(shù)對(duì)象,然后將兩者組成鍵值對(duì)存儲(chǔ)在當(dāng)前棧幀的 local 名字空間中,整體還是比較簡(jiǎn)單的。 但如果再加上類型注解、以及默認(rèn)值,會(huì)有什么效果呢? import dis
code_string = """ name = "satori" def foo(a: int = 1, b: int = 2): print(a, b) """
dis.dis(compile(code_string, "<func>", "exec"))
我們看看加上了類型注解和默認(rèn)值之后,它的字節(jié)碼指令會(huì)有什么變化? 0 RESUME 0
2 LOAD_CONST 0 ('satori') 4 STORE_NAME 0 (name)
6 LOAD_CONST 5 ((1, 2)) 8 LOAD_CONST 1 ('a') 10 LOAD_NAME 1 (int) 12 LOAD_CONST 2 ('b') 14 LOAD_NAME 1 (int) 16 BUILD_TUPLE 4 18 LOAD_CONST 3 (<code object foo at 0x7f...>) 20 MAKE_FUNCTION 5 (defaults, annotations) 22 STORE_NAME 2 (foo) ......
不難發(fā)現(xiàn),在構(gòu)建函數(shù)時(shí)會(huì)先將默認(rèn)值以元組的形式壓入運(yùn)行時(shí)棧;然后再將使用了類型注解的參數(shù)和類型也構(gòu)建一個(gè)元組,并壓入運(yùn)行時(shí)棧。 后續(xù)創(chuàng)建函數(shù)的時(shí)候,會(huì)將默認(rèn)值保存在 func_defaults 字段中,類型注解對(duì)應(yīng)的字典會(huì)保存在 func_annotations 字段中。 def foo(a: int = 1, b: int = 2): print(a, b)
print(foo.__defaults__) """ (1, 2) """ print(foo.__annotations__) """ {'a': <class 'int'>, 'b': <class 'int'>} """
基于類型注解,我們便可以額外施加一些手段,讓 Python 像靜態(tài)語(yǔ)言一樣,實(shí)現(xiàn)函數(shù)參數(shù)的類型約束。 這里再說(shuō)一下函數(shù)名,舉個(gè)例子。 def foo(): pass
print(foo.__name__) # foo
bar = foo print(bar.__name__) # foo
我們定義了一個(gè)函數(shù) foo,那么函數(shù)名就是 foo,這是沒(méi)問(wèn)題的,但怎么理解 bar 呢? 所以嚴(yán)格意義上講,代碼中的 foo 應(yīng)該是一個(gè)變量。之前說(shuō)過(guò),定義函數(shù)、類、導(dǎo)入模塊,其實(shí)都是創(chuàng)建了一個(gè)變量。所以代碼中的 foo 也是一個(gè)變量,它指向了函數(shù)對(duì)象,而函數(shù)的名字是保存在函數(shù)對(duì)象里面的。 code_string = """ def foo(): pass """
code_obj = compile(code_string, "<func>", "exec") # 我們是以模塊的形式編譯的,它里面只有一個(gè)變量 foo # 所以符號(hào)表就是 ('foo',) print(code_obj.co_names) # ('foo',)
# 然后常量池里面存在一個(gè) PyCodeObject # 這個(gè) PyCodeObject 便是函數(shù)對(duì)應(yīng)的 PyCodeObject print(code_obj.co_consts[0]) # <code object foo ...> print(code_obj.co_consts[0].co_name) # foo
# 構(gòu)建函數(shù)時(shí),PyCodeObject 的 co_name 會(huì)被賦值給函數(shù)的 func_name # 所以嚴(yán)格意義上講,def foo() 中的 foo 只能算做是變量名 # 而真正的函數(shù)名是函數(shù)對(duì)象的 func_name,它來(lái)自于 co_name # 只不過(guò)在編譯成 PyCodeObject 對(duì)象時(shí),會(huì)進(jìn)行詞法分析 # 因?yàn)?nbsp;def 后面是 foo,所以編譯之后的 PyCodeObject 的 co_name 也是 foo
# 當(dāng)然其它對(duì)象也是如此 class A: pass
# 這里的 A 指向了類型對(duì)象,但類型對(duì)象的名稱是保存在類型對(duì)象里面的 print(A.__name__) # A # A.__name__ 才是類名,class 后面的 A 只是一個(gè)變量名
# 這里同樣創(chuàng)建了一個(gè)類 B = type("B1", (object,), {}) print(B.__name__) # B1 # 但是我們看到類名不是 B,而是 B1 # 所以我們需要明白,不管是變量賦值、還是定義函數(shù)、類、方法,導(dǎo)入模塊 # 我們得到的只是一個(gè)變量,這個(gè)變量指向了具體的對(duì)象(它們是字典中的一個(gè)鍵值對(duì)) # 而對(duì)象的名稱、類型等信息,都保存在對(duì)象里面,和變量無(wú)關(guān) # 因?yàn)樽兞恐皇且粋€(gè)符號(hào),或者理解為代號(hào),每個(gè)對(duì)象都可以有不同的代號(hào)
def foo(): pass
# 名稱也可以自由更改 foo.__name__ = "foo1" # 在更改過(guò)后,函數(shù)的名字就變成了 foo1 print(foo.__name__) # foo1
# bar = foo 之后,這個(gè)函數(shù)對(duì)象就有了兩個(gè)代號(hào),你通過(guò) foo 和 bar 都可以找到它 # 但函數(shù)對(duì)象的名字是不變的,還是 foo1,因?yàn)樗?nbsp;__name__ 屬性的值是 foo1 bar = foo print(bar.__name__) # foo1
我們之前說(shuō)變量只是一個(gè)和對(duì)象綁定的符號(hào),或者說(shuō)代號(hào),運(yùn)行時(shí)會(huì)和某個(gè)對(duì)象(的地址)組成鍵值對(duì)保存在字典中。虛擬機(jī)通過(guò)變量可以找到它代表的對(duì)象,本質(zhì)上就是將變量名作為 key,去字典中檢索 value。至于獲取到的對(duì)象叫什么名字,是保存在對(duì)象里面的。 如果變量指向的是整數(shù)、字符串等,那么該對(duì)象就沒(méi)有名字。如果指向的是函數(shù)、類、模塊,那么對(duì)象的 __name__ 就是對(duì)象的名字。只不過(guò)在默認(rèn)情況下,定義函數(shù)(以及類)時(shí),變量名默認(rèn)和函數(shù)名是一樣的,所以我們會(huì)把指向函數(shù)對(duì)象的變量的名稱也叫做函數(shù)名。 關(guān)于這一點(diǎn),大家一定要清晰。 name = "古明地覺(jué)"
def foo(): pass
class A: pass
import os
print("name" in locals()) # True print("foo" in locals()) # True print("A" in locals()) # True print("os" in locals()) # True
這里的 name、foo、A、os 都是變量,站在虛擬機(jī)的角度,它們沒(méi)有任何的不同,只不過(guò)指向的對(duì)象不同罷了。而站在 Python 的角度,它們也是一樣的,其名稱都是字典里的一個(gè) key,只不過(guò)關(guān)聯(lián)的 value 不同罷了。 比如 name 指向的是字符串對(duì)象,foo 指向的是函數(shù)對(duì)象,A 指向的是類對(duì)象,os 指向的是模塊對(duì)象。但我們也可以改變指向,比如讓 foo 指向類對(duì)象,A 指向字符串對(duì)像等等,都是可以的。 總結(jié):變量只是一個(gè)指針,可以保存任意對(duì)象的地址,也就是可以指向任意的對(duì)象。而對(duì)象的名字、類型等一切信息,都保存在對(duì)象中,和變量無(wú)關(guān)。 當(dāng)然這些都是之前說(shuō)過(guò)的內(nèi)容,再來(lái)回顧一下,總之一定要了解 Python 變量的本質(zhì)。 我們通過(guò)一些騷操作,來(lái)更好地理解一下函數(shù)。
之前說(shuō) <class 'function'> 是函數(shù)的類型對(duì)象,而這個(gè)類底層沒(méi)有暴露給我們,但我們依舊可以通過(guò)曲線救國(guó)的方式進(jìn)行獲取。 def foo(): pass
print(type(foo)) # <class 'function'> # lambda 匿名函數(shù)的類型也是 function print(type(lambda: None)) # <class 'function'>
那么下面就來(lái)創(chuàng)建函數(shù): gender = "female"
def foo(name, age): return f"name: {name}, age: {age}, gender: {gender}"
# 得到 PyCodeObject 對(duì)象 code = foo.__code__ # 根據(jù) class function 創(chuàng)建函數(shù)對(duì)象 # 接收三個(gè)參數(shù): PyCodeObject 對(duì)象、名字空間、函數(shù)名 new_foo = type(foo)(code, globals(), "根據(jù) foo 創(chuàng)建的 new_foo")
# 打印函數(shù)名 print(new_foo.__name__) """ 根據(jù) foo 創(chuàng)建的 new_foo """
# 調(diào)用函數(shù) print(new_foo("古明地覺(jué)", 17)) """ name: 古明地覺(jué), age: 17, gender: female """
是不是很神奇呢?另外函數(shù)之所以能訪問(wèn)全局變量,是因?yàn)樵趧?chuàng)建函數(shù)的時(shí)候?qū)?global 名字空間傳進(jìn)去了,如果我們不傳遞呢? gender = "female"
def foo(name, age): return f"name: {name}, age: {age}, gender: {gender}"
code = foo.__code__ # 第二個(gè)參數(shù)必須是一個(gè)字典,不能傳 None new_foo = type(foo)(code, {}, "根據(jù) foo 創(chuàng)建的 new_foo")
try: print(new_foo("古明地覺(jué)", 17)) except NameError as e: print(e) # name 'gender' is not defined
因此現(xiàn)在我們又從 Python 的角度理解了一遍,為什么在函數(shù)內(nèi)部能夠訪問(wèn)全局變量。原因就在于構(gòu)建函數(shù)的時(shí)候,將 global 名字空間交給了函數(shù),使得函數(shù)可以在 global 空間中進(jìn)行變量查找,所以它才能夠找到全局變量。而我們這里給了一個(gè)空字典,那么顯然就找不到 gender 這個(gè)變量了。 gender = "female"
def foo(name, age): return f"name: {name}, age: {age}, gender: {gender}"
code = foo.__code__ new_foo = type(foo)(code, {"gender": "萌妹子"}, "根據(jù) foo 創(chuàng)建的 new_foo")
# 我們可以手動(dòng)傳遞一個(gè)字典進(jìn)去 # 此時(shí)傳遞的字典對(duì)于函數(shù)來(lái)說(shuō)就是 global 名字空間 print(new_foo("古明地覺(jué)", 17)) """ name: 古明地覺(jué), age: 17, gender: 萌妹子 """ # 所以此時(shí)的 gender 不再是外部的 "female", 而是我們指定的 "萌妹子"
此外也可以為函數(shù)指定默認(rèn)值: def foo(name, age, gender): return f"name: {name}, age: {age}, gender: {gender}"
# 必須接收一個(gè) PyTupleObject 對(duì)象 foo.__defaults__ = ("古明地覺(jué)", 17, "female") print(foo()) """ name: 古明地覺(jué), age: 17, gender: female """
我們看到函數(shù) foo 明明接收三個(gè)參數(shù),但是調(diào)用時(shí)不傳遞居然也不會(huì)報(bào)錯(cuò),原因就在于我們指定了默認(rèn)值。而默認(rèn)值可以在定義函數(shù)的時(shí)候指定,也可以通過(guò) __defaults__ 指定,但很明顯我們應(yīng)該通過(guò)前者來(lái)指定。 如果你使用的是 PyCharm,那么會(huì)在 foo() 這個(gè)位置給你加波浪線,提示你參數(shù)沒(méi)有傳遞。但我們知道,由于通過(guò) __defaults__ 設(shè)置了默認(rèn)值,所以這里是不會(huì)報(bào)錯(cuò)的。只不過(guò) PyCharm 沒(méi)有檢測(cè)到,當(dāng)然基本上所有的 IDE 都無(wú)法做到這一點(diǎn),畢竟動(dòng)態(tài)語(yǔ)言。 另外如果 __defaults__ 接收的元組里面的元素個(gè)數(shù)和參數(shù)個(gè)數(shù)不匹配怎么辦? def foo(name, age, gender): return f"name: {name}, age: {age}, gender: {gender}"
foo.__defaults__ = (15, "female") print(foo("古明地戀")) """ name: 古明地戀, age: 15, gender: female """
由于元組里面只有兩個(gè)元素,意味著我們?cè)谡{(diào)用時(shí)需要至少傳遞一個(gè)參數(shù),而這個(gè)參數(shù)會(huì)賦值給 name。原因就是在設(shè)置默認(rèn)值的時(shí)候是從后往前設(shè)置的,也就是 "female" 會(huì)賦值給 gender,15 會(huì)賦值給 age。而 name 沒(méi)有得到默認(rèn)值,那么它就需要調(diào)用者顯式傳遞了。 如果返回值從前往后設(shè)置的話,會(huì)出現(xiàn)什么后果?顯然 15 會(huì)賦值給 name,"female" 會(huì)賦值給 age,此時(shí)函數(shù)就等價(jià)于如下: def foo(name=15, age="female", gender): return f"name: {name}, age: {age}, gender: {gender}"
這樣的函數(shù)顯然無(wú)法通過(guò)編譯,因?yàn)槟J(rèn)參數(shù)必須在非默認(rèn)參數(shù)的后面。所以 Python 的這個(gè)做法是完全正確的,必須要從后往前進(jìn)行設(shè)置。 另外我們知道默認(rèn)值的個(gè)數(shù)是小于等于參數(shù)個(gè)數(shù)的,如果大于會(huì)怎么樣呢? def foo(name, age, gender): return f"name: {name}, age: {age}, gender: {gender}"
foo.__defaults__ = ("古明地覺(jué)", "古明地戀", 15, "female") print(foo()) """ name: 古明地戀, age: 15, gender: female """
依舊是從后往前進(jìn)行設(shè)置,當(dāng)所有參數(shù)都有默認(rèn)值時(shí),就結(jié)束了,多余的默認(rèn)值會(huì)丟棄。當(dāng)然,如果不使用 __defaults__,是不可能出現(xiàn)默認(rèn)值個(gè)數(shù)大于參數(shù)個(gè)數(shù)的。可要是 __defaults__ 指向的元組先結(jié)束,那么沒(méi)有得到默認(rèn)值的參數(shù)就必須由調(diào)用者顯式傳遞了。 最后,再來(lái)說(shuō)一下如何深拷貝一個(gè)函數(shù)。首先如果是你的話,你會(huì)怎么拷貝一個(gè)函數(shù)呢?不出意外的話,你應(yīng)該會(huì)使用 copy 模塊。 import copy
def foo(a, b): return [a, b]
# 但是問(wèn)題來(lái)了,這樣能否實(shí)現(xiàn)深度拷貝呢? new_foo = copy.deepcopy(foo) # 修改 foo 的默認(rèn)值 foo.__defaults__ = (2, 3) # 但是 new_foo 也會(huì)受到影響 print(new_foo()) # [2, 3]
打印結(jié)果提示我們并沒(méi)有實(shí)現(xiàn)函數(shù)的深度拷貝,事實(shí)上 copy 模塊無(wú)法對(duì)函數(shù)、方法、回溯棧、棧幀、模塊、文件、套接字等類型實(shí)現(xiàn)深度拷貝。那我們應(yīng)該怎么做呢? from types import FunctionType
def foo(a, b): return "result"
# FunctionType 就是函數(shù)的類型對(duì)象 # 它也是通過(guò) type 得到的 new_foo = FunctionType(foo.__code__, foo.__globals__, foo.__name__, foo.__defaults__, foo.__closure__) # 顯然 function 還可以接收第四個(gè)參數(shù)和第五個(gè)參數(shù) # 分別是函數(shù)的默認(rèn)值和閉包
# 然后別忘記將屬性字典也拷貝一份 # 由于函數(shù)的屬性字典幾乎用不上,這里就淺拷貝了 new_foo.__dict__.update(foo.__dict__)
foo.__defaults__ = (2, 3) print(foo.__defaults__) # (2, 3) print(new_foo.__defaults__) # None
此時(shí)修改 foo 不會(huì)影響 new_foo,當(dāng)然在拷貝的時(shí)候也可以自定義屬性。
其實(shí)上面實(shí)現(xiàn)的深拷貝,本質(zhì)上就是定義了一個(gè)新的函數(shù)。由于是兩個(gè)不同的函數(shù),那么自然就沒(méi)有聯(lián)系了。 最后再來(lái)看看如何檢測(cè)一個(gè)函數(shù)有哪些參數(shù),首先函數(shù)的局部變量(包括參數(shù))在編譯時(shí)就已經(jīng)確定,會(huì)存在符號(hào)表 co_varnames 中。 def foo(a, b, /, c, d, *args, e, f, **kwargs): g = 1 h = 2
print(foo.__code__.co_varnames) """ ('a', 'b', 'c', 'd', 'e', 'f', 'args', 'kwargs', 'g', 'h') """
在定義函數(shù)的時(shí)候,* 和 ** 最多只能出現(xiàn)一次。然后這里的 a 和 b 必須通過(guò)位置參數(shù)傳遞,c 和 d 可以通過(guò)位置參數(shù)或者關(guān)鍵字參數(shù)傳遞,e 和 f 必須通過(guò)關(guān)鍵字參數(shù)傳遞。 而從打印的符號(hào)表來(lái)看,里面的符號(hào)是有順序的。參數(shù)永遠(yuǎn)在函數(shù)內(nèi)部定義的局部變量的前面,比如 g 和 h 就是函數(shù)內(nèi)部定義的局部變量,所以它在所有參數(shù)的后面。而對(duì)于參數(shù),* 和 ** 會(huì)位于最后面,其它參數(shù)位置不變。所以除了 g 和 h,最后面的就是 args 和 kwargs。 有了這些信息,我們就可以進(jìn)行檢測(cè)了。 def foo(a, b, /, c, d, *args, e, f, **kwargs): g = 1 h = 2
varnames = foo.__code__.co_varnames # 1. 尋找必須通過(guò)位置參數(shù)傳遞的參數(shù) posonlyargcount = foo.__code__.co_posonlyargcount print(posonlyargcount) # 2 print(varnames[: posonlyargcount]) # ('a', 'b')
# 2. 尋找可以通過(guò)位置參數(shù)或者關(guān)鍵字參數(shù)傳遞的參數(shù) argcount = foo.__code__.co_argcount print(argcount) # 4 print(varnames[: argcount]) # ('a', 'b', 'c', 'd') print(varnames[posonlyargcount: argcount]) # ('c', 'd')
# 3. 尋找必須通過(guò)關(guān)鍵字參數(shù)傳遞的參數(shù) kwonlyargcount = foo.__code__.co_kwonlyargcount print(kwonlyargcount) # 2 print(varnames[argcount: argcount + kwonlyargcount]) # ('e', 'f')
# 4. 尋找 *args 和 **kwargs flags = foo.__code__.co_flags # 在介紹 PyCodeObject 的時(shí)候,我們說(shuō)里面有一個(gè) co_flags 成員 # 它是函數(shù)的標(biāo)識(shí),可以對(duì)函數(shù)類型和參數(shù)進(jìn)行檢測(cè) # 如果 co_flags 和 4 按位與之后為真,那么就代表有 *args,否則沒(méi)有 # 如果 co_flags 和 8 按位與之后為真,那么就代表有 **kwargs,否則沒(méi)有 step = argcount + kwonlyargcount if flags & 0x04: print(varnames[step]) # args step += 1
if flags & 0x08: print(varnames[step]) # kwargs
以上我們就檢測(cè)出了函數(shù)都有哪些參數(shù),你也可以將其封裝成一個(gè)函數(shù),實(shí)現(xiàn)代碼的復(fù)用。然后還要注意一點(diǎn),如果我們定義的時(shí)候不是 *args,而只是一個(gè) *,那么它就不是參數(shù)了。 def f(a, b, *, c): pass
# 符號(hào)表里面只有 a、b、c print(f.__code__.co_varnames) # ('a', 'b', 'c')
# 顯然此時(shí)也都為假 print(f.__code__.co_flags & 0x04) # 0 print(f.__code__.co_flags & 0x08) # 0
單獨(dú)的一個(gè) * 只是為了強(qiáng)制要求后面的參數(shù)必須通過(guò)關(guān)鍵字參數(shù)的方式傳遞。
這一次我們簡(jiǎn)單地分析了一下函數(shù)是如何創(chuàng)建的,并且還在 Python 的層面上做了一些小 trick。最后我們也分析了如何通過(guò) PyCodeObject 對(duì)象來(lái)檢索函數(shù)的參數(shù),以及相關(guān)種類,標(biāo)準(zhǔn)庫(kù)中的 inspect 模塊也是這么做的。準(zhǔn)確的說(shuō),是我們模仿人家的思路做的。 現(xiàn)在你是不是對(duì)函數(shù)有了一個(gè)更深刻的認(rèn)識(shí)了呢?當(dāng)然目前介紹的只是函數(shù)的一部分內(nèi)容,還有更多內(nèi)容等待我們挖掘,比如: 這些內(nèi)容我們接下來(lái)慢慢說(shuō)。
|