函數(shù)是任何一門(mén)編程語(yǔ)言都具備的基本元素,它可以將多個(gè)動(dòng)作組合起來(lái),一個(gè)函數(shù)代表了一系列的動(dòng)作。當(dāng)然我們之前說(shuō)函數(shù)也是一個(gè)變量,該變量指向一個(gè)函數(shù)。而且在調(diào)用函數(shù)時(shí)會(huì)干什么來(lái)著,沒(méi)錯(cuò),要在運(yùn)行時(shí)棧中創(chuàng)建棧幀,用于函數(shù)的執(zhí)行。
那么下面就來(lái)看看函數(shù)在C中是如何實(shí)現(xiàn)的,生得一副什么模樣。
我們說(shuō)過(guò)Python中一切皆對(duì)象,函數(shù)也不例外。在Python中,函數(shù)這種抽象機(jī)制是通過(guò)PyFunctionObject
對(duì)象實(shí)現(xiàn)的,位于 Include/funcobject.h 中。
typedef struct { PyObject_HEAD /* 頭部信息, 不用多說(shuō) */ PyObject *func_code; /* 函數(shù)的PyCodeObject對(duì)象, 因?yàn)楹瘮?shù)就是根據(jù)該P(yáng)yCodeObject對(duì)象創(chuàng)建的 */ PyObject *func_globals; /* 函數(shù)的global名字空間 */ PyObject *func_defaults; /* 函數(shù)參數(shù)的默認(rèn)值, 一個(gè)元組或者空 */ PyObject *func_kwdefaults; /* 只能通過(guò)關(guān)鍵字參數(shù)傳遞的"參數(shù)"和"該參數(shù)的默認(rèn)值", 一個(gè)字典或者空 */ PyObject *func_closure; /* 獲取閉包對(duì)象 */ PyObject *func_doc; /* 函數(shù)的doc */ PyObject *func_name; /* 函數(shù)名 */ PyObject *func_dict; /* 屬性字典, 一般為空 */ PyObject *func_weakreflist; /* 弱引用列表 */ PyObject *func_module; /* 函數(shù)所在的模塊 */ PyObject *func_annotations; /* 函數(shù)參數(shù)的注解, 一個(gè)字典或者空 */ PyObject *func_qualname; /* 函數(shù)的全限定名, 我們后面會(huì)說(shuō)它和func_name之間的區(qū)別 */ 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開(kāi)頭的,比如:func_name,但是我們?cè)赑ython中獲取的時(shí)候直接通過(guò)__name__獲取即可。
func_code:函數(shù)的字節(jié)碼
def foo(a, b, c): passcode = foo.__code__print(code) # <code object foo at 0x000001D250B9D3A0, file "C:/Users/satori/Desktop/三無(wú)少女/2.py", line 1>print(code.co_varnames) # ('a', 'b', 'c')
func_globals:global命名空間
def foo(a, b, c): passname = "夏色祭"# __globals__其實(shí)就是外部的global名字空間print(foo.__globals__) # {......, 'name': '夏色祭'}print(foo.__globals__ == globals()) # True
func_defaults:函數(shù)參數(shù)的默認(rèn)值
def foo(name="夏色祭", age=-1): pass# 打印的是默認(rèn)值print(foo.__defaults__) # ('夏色祭', -1)def bar(): pass# 沒(méi)有默認(rèn)值的話(huà), __defaults__為Noneprint(bar.__defaults__) # None
func_kwdefaults:只能通過(guò)關(guān)鍵字參數(shù)傳遞的"參數(shù)"和"該參數(shù)的默認(rèn)值"
def foo(name="夏色祭", age=-1): pass# 打印是為None的, 這是因?yàn)殡m然有默認(rèn)值, 但是它并不要求必須通過(guò)關(guān)鍵字的方式傳遞print(foo.__kwdefaults__) # None# 如果在前面加上一個(gè)*, 表示后面的參數(shù)就必須通過(guò)關(guān)鍵字的方式傳遞# 因?yàn)槿绻煌ㄟ^(guò)關(guān)鍵字的話(huà), 那么無(wú)論多少個(gè)位置參數(shù)都會(huì)被*給吸收掉, 無(wú)論如何也不可能傳遞給name、age# 我們經(jīng)常會(huì)看到*args, 這是因?yàn)槲覀冃枰瘮?shù)調(diào)用時(shí)傳遞過(guò)來(lái)的值, 所以可以通過(guò)args以元組的形式來(lái)拿到這些值# 但是這里我們不需要, 我們只是希望后面的參數(shù)必須通過(guò)關(guān)鍵字參數(shù)傳遞, 因此前面寫(xiě)一個(gè)*即可# 當(dāng)然寫(xiě)*args或者其他的也可以, 但是我們用不到, 所以寫(xiě)一個(gè)*即可def bar(*, name="夏色祭", age=-1): pass# 此時(shí)就打印了默認(rèn)值,因?yàn)檫@是只能通過(guò)kw(關(guān)鍵字)傳遞的參數(shù)的默認(rèn)值print(bar.__kwdefaults__) # {'name': 'satori', 'age': 16}
func_closure:閉包對(duì)象
def foo(): name = "夏色祭" age = -1 def bar(): nonlocal name nonlocal age return bar# 查看的是閉包里面nonlocal的值# 這里有兩個(gè)nonlocal,所以foo().__closure__是一個(gè)有兩個(gè)元素的元組print(foo().__closure__) # (<cell at 0x000001FD1D3B02B0: int object at 0x00007FFDE559D660>, # <cell at 0x000001FD1D42E310: str object at 0x000001FD1D3DA090>)print(foo().__closure__[0].cell_contents) # -1print(foo().__closure__[1].cell_contents) # 夏色祭# 注意:查看閉包屬性我們使用的是內(nèi)層函數(shù),不是外層的foo
func_doc:函數(shù)的文檔
def foo(name, age): """ 接收一個(gè)name和age, 返回一句話(huà) my name is $name, age is $age """ return f"my name is {name}, age is {age}"print(foo.__doc__)""" 接收一個(gè)name和age, 返回一句話(huà) my name is $name, age is $age """
func_name:函數(shù)名
def foo(name, age): passprint(foo.__name__) # foo
func_dict:屬性字典
def foo(name, age): pass# 一般函數(shù)的屬性字典都會(huì)空,屬性字典基本上在類(lèi)里面使用print(foo.__dict__) # {}
func_weakreflist:弱引用列表
Python無(wú)法獲取這個(gè)屬性,底層沒(méi)有提供相應(yīng)的接口。
func_module:函數(shù)所在的模塊
def foo(name, age): passprint(foo.__module__) # __main__
func_annotations:注解
def foo(name: str, age: int): pass# Python3.5的時(shí)候新增的語(yǔ)法print(foo.__annotations__) # {'name': <class 'str'>, 'age': <class 'int'>}
func_qualname:全限定名
def foo(): passprint(foo.__name__, foo.__qualname__) # foo fooclass A: def foo(self): passprint(A.foo.__name__, A.foo.__qualname__) # foo A.foo
在PyFunctionObject的定義中,我們看到一個(gè)func_code成員,指向了一個(gè)PyCodeObject對(duì)象,我們說(shuō)函數(shù)就是根據(jù)這個(gè)PyCodeObject對(duì)象創(chuàng)建的。因?yàn)槲覀冎酪粋€(gè)PyCodeObject對(duì)象是對(duì)一段代碼的靜態(tài)表示,Python編譯器在將源代碼進(jìn)行編譯之后,對(duì)里面的每一個(gè)代碼塊(code block)
都會(huì)生成一個(gè)、并且是唯一一個(gè)PyCodeObject對(duì)象,這個(gè)PyCodeObject對(duì)象中包含了這個(gè)代碼塊中的一些靜態(tài)信息,也就是可以從源代碼中看到的信息。
比如:某個(gè)函數(shù)對(duì)應(yīng)的code block中有一個(gè) name = "夏色祭" 這樣的表達(dá)式,那么符號(hào)"a"和對(duì)應(yīng)的值1、以及它們之間的聯(lián)系就是靜態(tài)信息。這些信息會(huì)被靜態(tài)存儲(chǔ)起來(lái),符號(hào)"a"會(huì)被存在符號(hào)表
co_varnames
中、值1會(huì)被存在常量池co_consts
中、這兩者之間是一個(gè)賦值,因此會(huì)有兩條指令LOAD_CONSTS和STORE_FAST存在字節(jié)碼指令序列co_code
中。這些信息是編譯的時(shí)候就可以得到的,因此PyCodeObject對(duì)象是編譯時(shí)候的結(jié)果。
但是PyFunctionObject對(duì)象是何時(shí)產(chǎn)生的呢?實(shí)際上PyFunctionObject對(duì)象是Python代碼在運(yùn)行時(shí)動(dòng)態(tài)產(chǎn)生的,更準(zhǔn)確的說(shuō),是在執(zhí)行一個(gè)def語(yǔ)句的時(shí)候創(chuàng)建的。
當(dāng)Python虛擬機(jī)在當(dāng)前棧幀中執(zhí)行字節(jié)碼時(shí)發(fā)現(xiàn)了def語(yǔ)句,那么就代表發(fā)現(xiàn)了新的PyCodeObject對(duì)象,因?yàn)樗鼈兪强梢詫訉忧短椎?。所以虛擬機(jī)會(huì)根據(jù)這個(gè)PyCodeObject對(duì)象創(chuàng)建對(duì)應(yīng)的PyFunctionObject對(duì)象,然后將函數(shù)名和函數(shù)體對(duì)應(yīng)的PyFunctionObject對(duì)象組成鍵值對(duì)放在當(dāng)前的local空間中。
顯然在PyFunctionObject對(duì)象中,也會(huì)包含這些函數(shù)的靜態(tài)信息,這些信息存儲(chǔ)在func_code中,實(shí)際上,func_code一定會(huì)指向與函數(shù)對(duì)應(yīng)的PyCodeObject對(duì)象。除此之外,PyFunctionObject對(duì)象中還包含了一些函數(shù)在執(zhí)行時(shí)所必須的動(dòng)態(tài)信息,即上下文信息。比如func_globals,就是函數(shù)在執(zhí)行時(shí)關(guān)聯(lián)的global作用域(globals),說(shuō)白了就是讓你在局部變量找不到的時(shí)候能夠找全局變量,可如果連global空間都沒(méi)有的話(huà),那即便想找也無(wú)從下手呀。而global作用域中的符號(hào)和值必須在運(yùn)行時(shí)才能確定,所以這部分必須在運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建,無(wú)法存儲(chǔ)在PyCodeObject中,所以要根據(jù)PyCodeObject對(duì)象創(chuàng)建PyFunctionObject對(duì)象,相當(dāng)于一個(gè)封裝??傊磺械哪康?,都是為了更好的執(zhí)行字節(jié)碼。
我們舉個(gè)栗子:
# 首先虛擬機(jī)從上到下執(zhí)行字節(jié)碼name = "夏色祭"age = -1# pia, 出現(xiàn)了一個(gè)defdef foo(): pass# 那么知道源代碼進(jìn)入了一個(gè)新的作用域了, 這里遇到一個(gè)新的PyCodeObject對(duì)象了# 而通過(guò)def知道這是一個(gè)函數(shù), 所以會(huì)進(jìn)行封裝, 將PyCodeObject對(duì)象封裝成PyFunctionObject# 所以當(dāng)執(zhí)行完def語(yǔ)句之后, 一個(gè)函數(shù)就被創(chuàng)建了, 放在當(dāng)前的local空間中, 當(dāng)然對(duì)于模塊來(lái)說(shuō): local空間也是global空間print(locals()) # {......, 'foo': <function foo at 0x000001B299FAF3A0>}# 函數(shù)的類(lèi)型是<class 'function'>, 當(dāng)然這個(gè)類(lèi)Python沒(méi)有暴露給我們# 當(dāng)我們調(diào)用函數(shù)foo的時(shí)候, 會(huì)從local空間中取出符號(hào)"foo"對(duì)應(yīng)的PyFunctionObject對(duì)象# 然后根據(jù)這個(gè)PyFunctionObject對(duì)象創(chuàng)建PyFrameObject對(duì)象, 也就是為函數(shù)創(chuàng)建一個(gè)棧幀# 然后將執(zhí)行權(quán)交給新創(chuàng)建的棧幀, 在新創(chuàng)建的棧幀中執(zhí)行字節(jié)碼
我們現(xiàn)在已經(jīng)看清了函數(shù)的模樣,它在底層對(duì)應(yīng)PyFunctionObject對(duì)象,并且它和PyCodeObject對(duì)象關(guān)系密切。那么Python底層又是如何完成PyCodeObject對(duì)象到PyFunctionObject對(duì)象之間的轉(zhuǎn)變呢?想了解這其中的奧秘,就必須要從字節(jié)碼入手。
s = """name = "夏色祭"def foo(a, b): print(a, b)foo(1, 2)"""import disdis.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_VALUEDisassembly 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
顯然這個(gè)代碼中出現(xiàn)了兩個(gè)PyCodeObject對(duì)象,一個(gè)對(duì)應(yīng)整個(gè)py文件,另一個(gè)則是對(duì)應(yīng)函數(shù)foo。
s = """name = "夏色祭"def foo(a, b): print(a, b)foo(1, 2)"""# 把字符串當(dāng)成是一個(gè)py文件來(lái)進(jìn)行編譯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
可以看到,"函數(shù)foo對(duì)應(yīng)的PyCodeObject對(duì)象"是"py文件對(duì)應(yīng)的PyCodeObject對(duì)象"的常量池co_consts中的一個(gè)元素。因?yàn)樵趯?duì)py文件創(chuàng)建PyCodeObject對(duì)象的時(shí)候,發(fā)現(xiàn)了一個(gè)函數(shù)代碼塊foo,那么會(huì)對(duì)函數(shù)代碼塊foo繼續(xù)創(chuàng)建一個(gè)PyCodeObject對(duì)象(每一個(gè)代碼塊都會(huì)對(duì)應(yīng)一個(gè)PyCodeObject對(duì)象),而函數(shù)foo對(duì)應(yīng)的PyCodeObject對(duì)象則是py文件對(duì)應(yīng)的PyCodeObject對(duì)象的co_consts常量池當(dāng)中的一個(gè)元素。
通過(guò)以上例子,我們發(fā)現(xiàn)PyCodeObject對(duì)象是嵌套的。之前我們我們說(shuō)過(guò),每一個(gè)code block
(函數(shù)、類(lèi)等等)
都會(huì)對(duì)應(yīng)一個(gè)PyCodeObject對(duì)象?,F(xiàn)在我們又看到了,根據(jù)層級(jí)來(lái)分的話(huà),"內(nèi)層代碼塊對(duì)應(yīng)的PyCodeObject對(duì)象"是"最近的外層代碼塊對(duì)應(yīng)的PyCodeObject對(duì)象"的常量池co_consts中的一個(gè)元素。而最外層則是模塊對(duì)應(yīng)的PyCodeObject對(duì)象,因此這就意味著我們通過(guò)最外層的PyCodeObject對(duì)象可以找到所有的PyCodeObject對(duì)象,顯然這是毋庸置疑的。而這里和棧幀也是對(duì)應(yīng)的,棧幀我們說(shuō)過(guò)也是層層嵌套的,而內(nèi)層棧幀通過(guò)f_back可以找到外層、也就是調(diào)用者對(duì)應(yīng)的棧幀,當(dāng)然這里我們之前的章節(jié)已經(jīng)說(shuō)過(guò)了,這里再提一遍。
這里再來(lái)重新看一下上面的字節(jié)碼:
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_VALUEDisassembly 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模塊自動(dòng)幫我們分成了兩部分,上面是模塊的字節(jié)碼,下面是函數(shù)的字節(jié)碼。首先函數(shù)很簡(jiǎn)單我們就不看了,直接看模塊的。
首先開(kāi)頭的LOAD_CONST和STORE_NAME顯然是 name = "夏色祭" 對(duì)應(yīng)的指令。然后我們看4?LOAD_CONST
,這條指令也是加載了一個(gè)常量,但這個(gè)常量是一個(gè)PyCodeObject對(duì)象;6?LOAD_CONST
則是將字符串常量"foo"、即函數(shù)名加載了進(jìn)來(lái),然后通過(guò)MAKE_FUNCTION指令構(gòu)建一個(gè)PyFunctionObject對(duì)象;然后10?STORE_NAME
,讓符號(hào)foo指向這個(gè)PyFunctionObject對(duì)象。再下面就是函數(shù)調(diào)用了,函數(shù)調(diào)用的具體細(xì)節(jié)我們之后會(huì)詳細(xì)說(shuō)。
并且我們還看到一個(gè)有趣的現(xiàn)象,那就是源代碼的行號(hào)。我們發(fā)現(xiàn)之前看到源代碼的行號(hào)都是從上往下、依次增大的,這很好理解,畢竟一條一條解釋嘛。但是這里卻發(fā)生了變化,先執(zhí)行了第6行,之后再執(zhí)行第4行。如果是從Python層面的函數(shù)調(diào)用來(lái)理解的話(huà),很容易一句話(huà)就解釋了,因?yàn)楹瘮?shù)只有在調(diào)用的時(shí)候才會(huì)執(zhí)行。但是從字節(jié)碼的角度來(lái)理解的話(huà),我們發(fā)現(xiàn)函數(shù)的聲明和實(shí)現(xiàn)是分離的,是在不同的PyCodeObject對(duì)象中。確實(shí)如此,雖然一個(gè)函數(shù)名和函數(shù)體是一個(gè)整體,但是Python虛擬機(jī)在實(shí)現(xiàn)這個(gè)函數(shù)的時(shí)候,卻在物理上將它們分離開(kāi)了,構(gòu)建函數(shù)的字節(jié)碼指令序列必須在模塊對(duì)應(yīng)的PyCodeObject對(duì)象中。
我們之前說(shuō)過(guò),函數(shù)即變量。我們是可以把函數(shù)當(dāng)成是普通的變量來(lái)處理的,函數(shù)名就相當(dāng)于變量名,函數(shù)體就相當(dāng)于是變量指向的值。而foo函數(shù)顯然是在全局中定義的一個(gè)函數(shù),那么foo是不是要出現(xiàn)在py文件對(duì)應(yīng)的PyCodeObject對(duì)象的符號(hào)表co_names里面呢?foo對(duì)應(yīng)的PyCodeObject對(duì)象是不是要出現(xiàn)在py文件對(duì)應(yīng)的PyCodeObject對(duì)象的常量池co_consts里面呢?
至此,函數(shù)的結(jié)構(gòu)就已經(jīng)非常清晰了。
所以函數(shù)名和函數(shù)體是分離的,它們存在不同的PyCodeObject對(duì)象當(dāng)中。分析完結(jié)構(gòu)之后,我們的重點(diǎn)就在于那個(gè)MAKE_FUNCTION指令了,我們說(shuō)當(dāng)遇到def?foo(a, b)
的時(shí)候,在語(yǔ)法上將這是函數(shù)的聲明語(yǔ)句,但是從虛擬機(jī)的角度來(lái)看這其實(shí)是函數(shù)對(duì)象的創(chuàng)建語(yǔ)句。所以下面我們就要分析一下這個(gè)指令,看看它到底是怎么將一個(gè)PyCodeObject對(duì)象變成一個(gè)PyFunctionObject對(duì)象的。
case TARGET(MAKE_FUNCTION): { PyObject *qualname = POP(); //彈出符號(hào)表中的函數(shù)名 PyObject *codeobj = POP(); //彈出對(duì)應(yīng)的字節(jié)碼對(duì)象 //創(chuàng)建PyFunctionObject對(duì)象, 接收三個(gè)參數(shù), 首先第一個(gè)參數(shù)和第三個(gè)參數(shù)很好理解, 但重點(diǎn)是第二個(gè)參數(shù) //首先f(wàn)指的就是當(dāng)前所在的棧幀, 對(duì)于我們目前這個(gè)里而言就是模塊、或者py文件對(duì)應(yīng)的棧幀 //然后將f_globals、也就是global名字空間傳遞了進(jìn)去, 所以我們現(xiàn)在明白了為什么函數(shù)可以調(diào)用__globals__了 //當(dāng)然也明白為什么函數(shù)可以在局部變量找不到的時(shí)候去找全局變量了 PyFunctionObject *func = (PyFunctionObject *) PyFunction_NewWithQualName(codeobj, f->f_globals, qualname); Py_DECREF(codeobj); Py_DECREF(qualname); if (func == NULL) { goto error; } //下面是設(shè)置閉包、注解、參數(shù)默認(rèn)值等屬性 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(); } //將函數(shù)或者說(shuō)函數(shù)對(duì)象壓入運(yùn)行時(shí)棧 PUSH((PyObject *)func); DISPATCH(); }
我們看到在MAKE FUNCTION之前,先進(jìn)行了LOAD CONST,顯然是將foo對(duì)應(yīng)的字節(jié)碼對(duì)象和符號(hào)foo壓入到了棧中。所以在執(zhí)行MAKE FUNCTION的時(shí)候,首先就是將這個(gè)字節(jié)碼對(duì)象以及對(duì)應(yīng)符號(hào)彈出棧,然后再加上當(dāng)前PyFrameObject對(duì)象中維護(hù)的global名字空間f_globals對(duì)象,三者作為參數(shù)傳入PyFunction_NewWithQualName函數(shù)中,從而構(gòu)建出相應(yīng)的PyFunctionObject對(duì)象。
下面我們來(lái)看看PyFunction_NewWithQualName是如何構(gòu)造出一個(gè)函數(shù)的,它位于 Objects/funcobject.c 中。
PyObject *PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname){ //要返回的PyFunctionObject *, 這里先聲明一下 PyFunctionObject *op; //函數(shù)的doc、PyCodeObject的co_consts、函數(shù)所在的模塊 PyObject *doc, *consts, *module; static PyObject *__name__ = NULL; if (__name__ == NULL) { __name__ = PyUnicode_InternFromString("__name__"); if (__name__ == NULL) return NULL; } //通過(guò)PyObject_GC_New為函數(shù)對(duì)象申請(qǐng)空間 op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type); if (op == NULL) return NULL; //下面就是設(shè)置PyFunctionObject對(duì)象的成員屬性了 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; //通過(guò)PyCodeObject對(duì)象獲取常量池 consts = ((PyCodeObject *)code)->co_consts; //我們知道函數(shù)的doc其實(shí)就是一個(gè)字符串, 顯然它也是常量池的一個(gè)常量, 并且是常量池的第一個(gè)元素 //否則的話(huà)它就是不能成為doc if (PyTuple_Size(consts) >= 1) { //所以如果consts>=1, 并且第一個(gè)元素是字符串, 那么它就是函數(shù)的doc doc = PyTuple_GetItem(consts, 0); if (!PyUnicode_Check(doc)) doc = Py_None; } else //否則doc就是None doc = Py_None; Py_INCREF(doc); //下面也是設(shè)置PyFunctionObject對(duì)象的成員 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;}
所以通過(guò)MAKE_FUNCTION我們便創(chuàng)建了PyFunctionObject對(duì)象,然后它會(huì)被壓入棧中,再通過(guò)STORE_NAME將符號(hào)foo和PyFunctionObject對(duì)象組成一個(gè)entry,存儲(chǔ)在當(dāng)前棧幀的local名字空間中,當(dāng)然也是global名字空間。只不過(guò)為了和函數(shù)保持統(tǒng)一,我們都說(shuō)成local名字空間,只不過(guò)不同的作用域?qū)?yīng)的local空間是不一樣的。
當(dāng)然了我們說(shuō)函數(shù)對(duì)象的類(lèi)型是<class 'function'>
,但是這個(gè)類(lèi)底層沒(méi)有暴露給我們,但是我們依舊可以通過(guò)曲線(xiàn)救國(guó)的方式進(jìn)行獲取。
def f(): passprint(type(f)) # <class 'function'># lambda匿名函數(shù)的類(lèi)型也是<class 'function'>print(type(lambda: None)) # <class 'function'>
所以我們可以仿照底層的思路,通過(guò)<class 'function'>
來(lái)創(chuàng)建一個(gè)函數(shù)對(duì)象。
gender = "female"def f(name, age): return f"name: {name}, age: {age}, gender: {gender}"# 得到PyCodeObject對(duì)象code = f.__code__# 根據(jù)class function創(chuàng)建函數(shù)對(duì)象, 接收三個(gè)參數(shù): PyCodeObject對(duì)象、名字空間、函數(shù)名new_f = type(f)(code, globals(), "根據(jù)f創(chuàng)建的new_f")# 打印函數(shù)名print(new_f.__name__) # 根據(jù)f創(chuàng)建的new_f# 調(diào)用函數(shù)print(new_f("夏色祭", -1)) # name: 夏色祭, age: -1, gender: female
是不是很神奇呢?另外我們說(shuō)函數(shù)在訪問(wèn)gender指向的對(duì)象時(shí),顯然先從自身的符號(hào)表中找,如果沒(méi)有那么回去找全局變量。這是因?yàn)?,我們?cè)趧?chuàng)建函數(shù)的時(shí)候?qū)lobal名字空間傳進(jìn)去了,如果我們不傳遞呢?
gender = "female"def f(name, age): return f"name: {name}, age: {age}, gender: {gender}"code = f.__code__try: new_f = type(f)(code, None, "根據(jù)f創(chuàng)建的new_f")except TypeError as e: print(e) # function() argument 'globals' must be dict, not None# 這里告訴我們function的第二個(gè)參數(shù)globals必須是一個(gè)字典# 我們傳遞一個(gè)空字典new_f1 = type(f)(code, {}, "根據(jù)f創(chuàng)建的new_f1")# 打印函數(shù)名print(new_f1.__name__) # 根據(jù)f創(chuàng)建的new_f1# 調(diào)用函數(shù)try: print(new_f1("夏色祭", -1))except NameError as e: print(e) # name 'gender' is not defined# 我們看到告訴我們gender沒(méi)有定義
因此現(xiàn)在我們又在Python的角度上理解了一遍,為什么Python中的函數(shù)能夠在局部變量找不到的時(shí)候,去找全局變量,原因就在于構(gòu)建函數(shù)的時(shí)候,將global名字空間交給了函數(shù)。使得函數(shù)可以在global空間進(jìn)行變量查找,所以它才能夠找到全局變量。而我們這里給了一個(gè)空字典,那么顯然就找不到gender這個(gè)變量了。
gender = "female"def f(name, age): return f"name: {name}, age: {age}, gender: {gender}"code = f.__code__new_f = type(f)(code, {"gender": "萌妹子"}, "根據(jù)f創(chuàng)建的new_f")# 我們可以手動(dòng)傳遞一個(gè)字典進(jìn)去, 此時(shí)我們傳遞的字典對(duì)于函數(shù)來(lái)說(shuō)就是global名字空間# 所以在函數(shù)內(nèi)部找不到某個(gè)變量的時(shí)候, 就會(huì)去我們指定的名字空間中找print(new_f("夏色祭", -1)) # name: 夏色祭, age: -1, gender: 萌妹子# 所以此時(shí)的gender不再是外部的"female", 而是我們指定的"萌妹子"
此外我們還可以為函數(shù)指定默認(rèn)值:
gender = "female"def f(name, age): return f"name: {name}, age: {age}, gender: {gender}"code = f.__code__new_f = type(f)(code, {"gender": "屑女仆"}, "根據(jù)f創(chuàng)建的new_f")# 必須接收一個(gè)PyTupleObject對(duì)象new_f.__defaults__ = ("神樂(lè)mea", 38)# 即使我們不傳遞參數(shù), 也是完全可以的, 因?yàn)橐呀?jīng)有默認(rèn)值了print(new_f()) # name: 神樂(lè)mea, age: 38, gender: 屑女仆# 我們也可以指定部分默認(rèn)參數(shù)new_f1 = type(f)(code, {"gender": "屑女仆"}, "根據(jù)f創(chuàng)建的new_f1")# 這里的在設(shè)置默認(rèn)值的時(shí)候是從后往前設(shè)置的, 比如: ("神樂(lè)mea", 38)# 是將38設(shè)置為age的默認(rèn)值, "神樂(lè)mea"設(shè)置為name的默認(rèn)值# 所以這里的(38,) , 會(huì)將38設(shè)置為age的默認(rèn)值, 不是name# 那name怎么辦? 如果沒(méi)有對(duì)應(yīng)的默認(rèn)值了, 那么它就必須在函數(shù)調(diào)用的時(shí)候由我們顯式的傳遞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: 屑女仆"""但是問(wèn)題來(lái)了, 為什么在設(shè)置默認(rèn)值的時(shí)候要從后往前呢?首先如果默認(rèn)值的個(gè)數(shù)和參數(shù)的個(gè)數(shù)正好匹配, 那么相安無(wú)事, 如果不匹配那么只能是默認(rèn)值的個(gè)數(shù)小于參數(shù)個(gè)數(shù)如果是從后往前, 那么(38,)就意味著38設(shè)置為age的默認(rèn)值, name就必須由我們?cè)谡{(diào)用的時(shí)候傳遞但如果是從前往后, 那么(38,)就意味著38設(shè)置為name的默認(rèn)值, age就必須由我們?cè)谡{(diào)用的時(shí)候來(lái)傳遞但是問(wèn)題來(lái)了, 如果38設(shè)置為name的默認(rèn)值, 這會(huì)是什么情況? 顯然等價(jià)于:def new_f1(name=38, age): ...... 你認(rèn)為這樣的函數(shù)能夠通過(guò)編譯嗎?顯然是不行的, 因?yàn)槟J(rèn)參數(shù)必須在非默認(rèn)參數(shù)的后面"""# 所以Python的這個(gè)做法是完全正確的, 必須要從后往前進(jìn)行設(shè)置
當(dāng)然,這種設(shè)置默認(rèn)值的方式顯然也可以使用于通過(guò)def定義的函數(shù),因?yàn)槲覀兩厦娴膎ew_f、new_f1和f都是<class 'function'>
對(duì)象。
gender = "female"def f(name, age): return f"name: {name}, age: {age}, gender: {gender}"print(f.__defaults__) # None# 設(shè)置默認(rèn)值f.__defaults__ = ("夏色祭", -1)# 如果你用的是pycharm, 那么會(huì)在f()這個(gè)位置給你做上標(biāo)記, 提示你參數(shù)沒(méi)有傳遞# 但我們知道由于使用__defaults__已經(jīng)設(shè)置了默認(rèn)值, 所以這里是不會(huì)報(bào)錯(cuò)的, 只不過(guò)pycharm沒(méi)有檢測(cè)到, 當(dāng)然基本上所有的ide都無(wú)法做到這一點(diǎn)print(f()) # name: 夏色祭, age: -1, gender: female
另外我們說(shuō),默認(rèn)值的個(gè)數(shù)一定要小于等于參數(shù)的個(gè)數(shù),但如果大于呢?
gender = "female"def f(name, age): return f"name: {name}, age: {age}, gender: {gender}"print(f.__defaults__) # Nonef.__defaults__ = ("夏色祭", -1, "神樂(lè)mea", 38)print(f()) # name: 神樂(lè)mea, age: 38, gender: female# 依舊從后往前, 38給age、"神樂(lè)mea"給name# 參數(shù)都有默認(rèn)值了, 那么就結(jié)束了# 當(dāng)然如果是__defaults__指向的元組先結(jié)束, 那么沒(méi)有得到默認(rèn)值的參數(shù)就必須由我們來(lái)傳遞了
想不到Python中的函數(shù)可以玩出這么多新花樣,現(xiàn)在你是不是對(duì)函數(shù)有了一個(gè)更深刻的認(rèn)識(shí)了呢?當(dāng)然目前介紹的只是函數(shù)的一小部分內(nèi)容,還有函數(shù)如何調(diào)用、位置參數(shù)和關(guān)鍵字參數(shù)如何解析、對(duì)于有默認(rèn)值的參數(shù)如何在我們不傳遞的時(shí)候使用默認(rèn)值以及在我們傳遞的時(shí)候使用我們傳遞的值、*args和**kwargs又如何解析、閉包怎么做到的、還有裝飾器等等等等,這些我們接下來(lái)會(huì)單獨(dú)用幾篇博客詳細(xì)說(shuō)。
因?yàn)榉旁谝黄┛屠锩娴脑?huà),字?jǐn)?shù)至少要好幾萬(wàn),而我使用的Markdown編輯器typora在字?jǐn)?shù)達(dá)到一萬(wàn)五的時(shí)候就會(huì)出現(xiàn)明顯卡頓,要是一下子都寫(xiě)完的話(huà),絕對(duì)卡到爆,而且越往后越卡,這對(duì)我而言也是個(gè)痛苦。而且函數(shù)的內(nèi)容也比較多,我們就多用一些篇幅去介紹它吧。
最后我們?cè)賮?lái)看看我們?nèi)绾螜z測(cè)一個(gè)函數(shù)有哪些參數(shù),首先函數(shù)的局部變量(包括參數(shù))
在編譯是就已經(jīng)確定,會(huì)存在符號(hào)表co_varnames中。
# 注意: 在定義函數(shù)的時(shí)候*和**最多只能出現(xiàn)一次# 顯然a和b必須通過(guò)位置參數(shù)傳遞# c和d可以通過(guò)位置參數(shù)或者關(guān)鍵字參數(shù)傳遞# e和f必須通過(guò)關(guān)鍵字參數(shù)傳遞def f(a, b, /, c, d, *args, e, f, **kwargs): g = 1 h = 2varnames = f.__code__.co_varnamesprint(varnames)# ('a', 'b', 'c', 'd', 'e', 'f', 'args', 'kwargs', 'g', 'h')"""首先co_varnames打印的符號(hào)表是有順序的, 參數(shù)永遠(yuǎn)在函數(shù)內(nèi)部定義的局部變量的前面g和h就是函數(shù)內(nèi)部定義的局部變量, 所以它在所有的后面如果是參數(shù)的話(huà), 那么*和**會(huì)位于最后面, 其它參數(shù)位置不變, 所以除了g和h, 最后面的就是args和kwargs"""# 接下來(lái), 我們就可以進(jìn)行判斷了# 1. 尋找必須通過(guò)位置參數(shù)傳遞的參數(shù)posonlyargcount = f.__code__.co_posonlyargcountprint(posonlyargcount) # 2print(varnames[: posonlyargcount]) # ('a', 'b')# 2. 尋找可以通過(guò)位置參數(shù)傳遞或者關(guān)鍵字參數(shù)傳遞的參數(shù)argcount = f.__code__.co_argcountprint(argcount) # 4print(varnames[: 4]) # ('a', 'b', 'c', 'd')print(varnames[posonlyargcount: 4]) # ('c', 'd')# 3. 尋找必須通過(guò)關(guān)鍵字參數(shù)傳遞的參數(shù)kwonlyargcount = f.__code__.co_kwonlyargcountprint(kwonlyargcount) # 2print(varnames[argcount: argcount kwonlyargcount]) # ('e', 'f')# 4. 尋找*args和**kwargs"""在介紹PyCodeObject對(duì)象的時(shí)候, 我們說(shuō)里面有一個(gè)co_flags成員它是專(zhuān)門(mén)用來(lái)判斷參數(shù)中是否有*args和**kwargs的"""flags = f.__code__.co_flags# 如果flags和4進(jìn)行按位與之后為真, 那么就代表有*args, 否則沒(méi)有# 如果flags和8進(jìn)行按位與之后為真, 那么就代表有**kwargs, 否則沒(méi)有step = argcount kwonlyargcountif flags & 0x04: print(varnames[step]) # args step = 1if flags & 0x08: print(varnames[step]) # kwargs # 雖然我們這里打印的是args和kwargs, 但主要取決定義的時(shí)候使用的名字# 如果定義的時(shí)候是*ARGS和**KWARGS, 那么這里就是ARGS和KWARGS, 只不過(guò)一般我們都叫做*args和**kwargs
如果我們定義的不是*args,只是一個(gè)*,那么它就不是參數(shù)了。
def f(a, b, *, c): pass# 我們看到此時(shí)只有a、b、cprint(f.__code__.co_varnames) # ('a', 'b', 'c')print(f.__code__.co_flags & 0x04) # 0print(f.__code__.co_flags & 0x08) # 0# 顯然此時(shí)也都為假
這一次我們簡(jiǎn)單的分析了一下函數(shù)在底層對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),以及如何創(chuàng)建一個(gè)函數(shù),并且還在Python的層面上做了一些小trick。最后我們也分析了如何通過(guò)PyCodeObject對(duì)象來(lái)檢索Python中的參數(shù),以及相關(guān)種類(lèi),當(dāng)然標(biāo)準(zhǔn)庫(kù)中的inspect模塊也是這么做的。當(dāng)然說(shuō)白了,其實(shí)是我們模仿人家的思路做的。
來(lái)源:https://www.icode9.com/content-1-729601.html聯(lián)系客服