楔子 在之前的文章中一直反復(fù)提到四個(gè)字:名字空間。一段代碼執(zhí)行的結(jié)果不光取決于代碼中的符號(hào),更多的是取決于代碼中符號(hào)的語(yǔ)義,而這個(gè)運(yùn)行時(shí)的語(yǔ)義正是由名字空間決定的。 名字空間是由虛擬機(jī)在運(yùn)行時(shí)動(dòng)態(tài)維護(hù)的,但有時(shí)我們希望將名字空間靜態(tài)化。換句話說(shuō),我們希望有的代碼不受名字空間變化帶來(lái)的影響,始終保持一致的功能該怎么辦呢?隨便舉個(gè)例子:
我們注意到每次都需要輸入 username 和 password,于是可以通過(guò)使用嵌套函數(shù)來(lái)設(shè)置一個(gè)基準(zhǔn)值。
盡管函數(shù) login 里面沒(méi)有 user_name 和 password 這兩個(gè)局部變量,但是不妨礙我們使用它,因?yàn)橥鈱雍瘮?shù) deco 里面有。 也就是說(shuō),函數(shù) login 作為函數(shù) deco 的返回值被返回的時(shí)候,有一個(gè)名字空間就已經(jīng)和 login 緊緊地綁定在一起了。執(zhí)行內(nèi)層函數(shù) login 的時(shí)候,對(duì)于自身 local 空間中不存在的變量,會(huì)從和自己綁定的 local 空間里面去找,這就是一種將名字空間靜態(tài)化的方法。這個(gè)名字空間和內(nèi)層函數(shù)捆綁之后的結(jié)果我們稱之為閉包(closure)。
也就是說(shuō):閉包=外部作用域+內(nèi)層函數(shù)。并且在介紹函數(shù)的時(shí)候提到,PyFunctionObject 是虛擬機(jī)專門為字節(jié)碼指令的傳輸而準(zhǔn)備的大包袱,global 名字空間、默認(rèn)參數(shù)都和字節(jié)碼指令捆綁在一起,同樣的,也包括閉包。 實(shí)現(xiàn)閉包的基石 閉包的創(chuàng)建通常是利用嵌套函數(shù)來(lái)完成的,我們說(shuō)過(guò)局部變量是通過(guò)數(shù)組靜態(tài)存儲(chǔ)的,而閉包也是如此。這里再來(lái)回顧一下 PyCodeObject 里面的幾個(gè)關(guān)鍵字段:
因此不難得出它們之間的關(guān)系:
那么這些變量的值都存在什么地方呢?沒(méi)錯(cuò)就是棧幀的 localsplus 字段中。 我們看一段代碼:
和閉包相關(guān)的兩個(gè)字段是 co_cellvars 和 co_freevars。co_cellvars 保存了外層作用域中被內(nèi)層作用域引用的變量的名字,co_freevars 保存了內(nèi)層作用域中引用的外層作用域的變量的名字。 所以對(duì)于外層函數(shù)來(lái)說(shuō),應(yīng)該使用 co_cellvars,對(duì)于內(nèi)層函數(shù)來(lái)說(shuō),應(yīng)該使用 co_freevars。當(dāng)然無(wú)論是外層函數(shù)還是內(nèi)層函數(shù)都有 co_cellvars 和 co_freevars,這是肯定的,因?yàn)槎际呛瘮?shù)。 只不過(guò)外層函數(shù)需要使用 co_cellvars 獲取,因?yàn)樗氖峭鈱雍瘮?shù)中被內(nèi)層函數(shù)引用的變量的名稱;內(nèi)層函數(shù)需要使用 co_freevars 獲取,它包含的是內(nèi)層函數(shù)中引用的外層函數(shù)的變量的名稱。 如果使用外層函數(shù) foo 獲取 co_freevars 的話,那么得到的結(jié)果顯然就是個(gè)空元組了,除非 foo 也作為某個(gè)函數(shù)的內(nèi)層函數(shù),并且內(nèi)部引用了外層函數(shù)的變量。同理內(nèi)層函數(shù) bar 也是一樣的道理,它獲取 co_cellvars 得到的也是空元組,因?yàn)閷?duì)于 bar 而言不存在內(nèi)層函數(shù)。 我們?cè)倏磦€(gè)例子:
對(duì)于函數(shù) bar 而言,它是函數(shù) inner 的外層函數(shù),同時(shí)也是函數(shù) foo 的內(nèi)層函數(shù)。所以它在獲取 co_cellvars 和 co_freevars 屬性時(shí),得到的元組都不為空。因?yàn)閮?nèi)層函數(shù) inner 引用了函數(shù) bar 里面的變量 gender,同時(shí)函數(shù) bar 也作為內(nèi)層函數(shù)引用了函數(shù) foo 里的 name 和 age。 那么問(wèn)題來(lái)了,閉包變量所需要的空間申請(qǐng)?jiān)谀膫€(gè)地方呢?沒(méi)錯(cuò),顯然是 localsplus。
localplus 是一個(gè)柔性數(shù)組,它被分成了四份,分別用于:局部變量、cell 變量、free 變量、運(yùn)行時(shí)棧。 所以閉包變量同樣是以靜態(tài)的方式實(shí)現(xiàn)的。 閉包的實(shí)現(xiàn)過(guò)程 介紹完實(shí)現(xiàn)閉包的基石之后,我們可以開(kāi)始追蹤閉包的具體實(shí)現(xiàn)過(guò)程了,當(dāng)然還是要先看一下閉包對(duì)應(yīng)的字節(jié)碼。
字節(jié)碼指令如下,為了閱讀方便,我們省略了源代碼行號(hào)。
字節(jié)碼的內(nèi)容并不難,我們來(lái)分析一下,這里先分析外層函數(shù) some_func 對(duì)應(yīng)的字節(jié)碼。 函數(shù) some_func 里面有三個(gè)局部變量,但只有 name 和 age 被內(nèi)層函數(shù)引用了,所以開(kāi)頭有兩個(gè) MAKE_CELL 指令。參數(shù)為符號(hào)在符號(hào)表中的索引,對(duì)應(yīng)的符號(hào)分別為 age 和 name。我們來(lái)看一下這個(gè)指令是做什么的。
所以 MAKE_CELL 指令的作用是創(chuàng)建 PyCellObject,對(duì)于當(dāng)前來(lái)說(shuō),會(huì)創(chuàng)建兩個(gè) PyCellObejct,它們的 ob_ref 字段分別為 age 和 name。只不過(guò)由于 name 和 age 還尚未完成賦值,所以此時(shí)為 NULL。 接下來(lái)就是變量賦值,這個(gè)顯然沒(méi)什么難度,我們只需要看一下 STORE_DEREF 指令。并且也容易得出結(jié)論,如果局部變量被內(nèi)層函數(shù)所引用,那么指令將不再是 LOAD_FAST 和 STORE_FAST,而是 LOAD_DEREF 和 STORE_DEREF。
localplus 保存了局部變量的值,而符號(hào)在符號(hào)表中的索引,和對(duì)應(yīng)的值在 localplus 中的索引是一致的。所以正常情況下,局部變量賦值就是 localsplus[oparg] = v。 但在執(zhí)行 MAKE_CELL 指令之后,局部變量賦值就變成了 localsplus[oparg]->ob_ref = v,因?yàn)榇藭r(shí) localplus 保存的是 PyCellObject 的地址。 因此在兩個(gè) STORE_DEREF 執(zhí)行完之后,localplus 會(huì)變成下面這樣。 相信你明白 STORE_FAST 和 STORE_DEREF 之間的區(qū)別了,如果是 STORE_FAST,那么中間就沒(méi)有 PyCellObject 這一層,localsplus 保存的 PyObject * 指向的就是具體的對(duì)象。 然后是 gender = "female",它就很簡(jiǎn)單了,由于符號(hào) "gender" 在符號(hào)表中的索引為 0,那么直接讓 localplus[0] 指向字符串 "female" 即可。 到此變量 name、age、gender 均已賦值完畢,此時(shí) localsplus 結(jié)構(gòu)如下。 localsplus[0]、localsplus[2]、localsplus[3] 分別對(duì)應(yīng)變量 gender、age、name,可能有人覺(jué)得,這個(gè)索引好奇怪啊,我們實(shí)際測(cè)試一下。
我們看到 some_func 的符號(hào)表里面只有 gender 和 inner,因此 localplus[0] 表示變量 gender。至于 localplus[1] 則表示變量 inner,只不過(guò)此時(shí)它指向的對(duì)象還沒(méi)有創(chuàng)建,所以暫時(shí)為 NULL。 那么問(wèn)題來(lái)了,變量 name 和 age 呢?毫無(wú)疑問(wèn),由于它們被內(nèi)層函數(shù)引用了,所以它們變成了 cell 變量,并且位置是 co->co_nlocals + i。因?yàn)樵?nbsp;localsplus 中,cell 變量的位置是在局部變量之后的,這也完全符合我們之前說(shuō)的 localsplus 的內(nèi)存布局。 并且我們看到無(wú)論是局部變量還是 cell 變量,都是通過(guò)數(shù)組索引訪問(wèn)的,并且索引在編譯時(shí)就確定了,以指令參數(shù)的形式保存在字節(jié)碼指令集中。 接下來(lái)執(zhí)行偏移量為 18 和 20 的兩條指令,它們都是 LOAD_CLOSURE。
LOAD_CLOSURE 執(zhí)行完畢后,接著執(zhí)行 BUILD_TUPLE,將 cell 變量從棧中彈出,構(gòu)建元組。然后繼續(xù)執(zhí)行 24 LOAD_CONST,將內(nèi)層函數(shù) inner 對(duì)應(yīng)的 PyCodeObject 壓入運(yùn)行時(shí)棧。 接著執(zhí)行 26 MAKE_FUNCTION,將棧中元素彈出,分別是 inner 對(duì)應(yīng)的 PyCodeObject 和一個(gè)元組,元組里面包含了 inner 使用的外層函數(shù)的變量。當(dāng)然這里的變量已經(jīng)不再是普通的變量了,而是 cell 變量,它內(nèi)部的 ob_ref 字段才是我們需要的。 等元素彈出之后,開(kāi)始構(gòu)建函數(shù),我們看一下 MAKE_FUNCTION 指令,它的指令參數(shù)為 8。
所以 PyFunctionObject 再一次承擔(dān)了工具人的角色,創(chuàng)建內(nèi)層函數(shù) inner 時(shí),會(huì)將包含 cell 變量的元組賦值給 func_closure 字段。此時(shí)便將內(nèi)層函數(shù)需要使用的變量和內(nèi)層函數(shù)綁定在了一起,而這個(gè)綁定的結(jié)果我們就稱之為閉包。 但是從結(jié)構(gòu)上來(lái)看,閉包仍是一個(gè)函數(shù),所謂綁定,其實(shí)只是修改了它的 func_closure 字段。當(dāng)函數(shù)創(chuàng)建完畢后,localplus 的結(jié)構(gòu)變化如下。 函數(shù)即變量,對(duì)于函數(shù) some_func 而言,內(nèi)層函數(shù) inner 也是一個(gè)局部變量,由于符號(hào) inner 位于符號(hào)表中索引為 1 的位置。因此當(dāng)函數(shù)創(chuàng)建完畢時(shí),會(huì)修改 localplus[1],讓它保存函數(shù)的地址。不難發(fā)現(xiàn),對(duì)于局部變量來(lái)說(shuō),如何訪問(wèn)內(nèi)存在編譯階段就確定了。 函數(shù)內(nèi)部的 func_closure 字段指向一個(gè)元組,元組里面的每個(gè)元素會(huì)指向 PyCellObject。 調(diào)用閉包 閉包的創(chuàng)建過(guò)程我們已經(jīng)了解了,我們用 Python 代碼再解釋一下。
調(diào)用 inner 函數(shù)時(shí),外層函數(shù) some_func 已經(jīng)執(zhí)行結(jié)束,但它的局部變量 name 和 age 仍可被內(nèi)層函數(shù) inner 訪問(wèn),背后的原因我們算是徹底明白了。 因?yàn)?name 和 age 被內(nèi)層函數(shù)引用了,所以虛擬機(jī)將它們封裝成了 PyCellObject *,即 cell 變量,而 cell 變量指向的 cell 對(duì)象內(nèi)部的 ob_ref 字段對(duì)應(yīng)原來(lái)的變量。當(dāng)創(chuàng)建內(nèi)層函數(shù)時(shí),將引用的 cell 變量組成元組,保存在內(nèi)層函數(shù)的 func_closure 字段中。 所以當(dāng)內(nèi)層函數(shù)在訪問(wèn) name 和 age 時(shí),訪問(wèn)的其實(shí)是 PyCellObject 的 ob_ref 字段。至于變量 name 和 age 對(duì)應(yīng)哪一個(gè) PyCellObject,這些在編譯階段便確定了,我們看一下內(nèi)層函數(shù) inner 的字節(jié)碼指令。 函數(shù)在執(zhí)行時(shí)會(huì)創(chuàng)建棧幀,我們上面看到的 localsplus 是外層函數(shù) some_func 對(duì)應(yīng)的棧幀的 localsplus。而內(nèi)層函數(shù) inner 執(zhí)行時(shí),也會(huì)創(chuàng)建棧幀,然后在棧幀中執(zhí)行字節(jié)碼指令。 首先第一個(gè)指令是 COPY_FREE_VARS,看一下它的邏輯。
處理完之后,localplus 的布局如下,注意:此時(shí)是內(nèi)層函數(shù)對(duì)應(yīng)的 localplus。 在構(gòu)建內(nèi)層函數(shù)時(shí),會(huì)將 cell 變量打包成一個(gè)元組,交給內(nèi)層函數(shù)的 func_closure 字段。然后執(zhí)行內(nèi)層函數(shù)創(chuàng)建棧幀的時(shí)候,再將 func_closure 中的 cell 變量拷貝到 localsplus 的第三段內(nèi)存中。當(dāng)然對(duì)于內(nèi)層函數(shù)而言,此時(shí)它應(yīng)該叫做 free 變量。 而在調(diào)用內(nèi)層函數(shù) inner 的過(guò)程中,當(dāng)引用外層作用域的符號(hào)時(shí),一定是到 localsplus 里面的 free 區(qū)域(第三段內(nèi)存)去獲取對(duì)應(yīng)的 PyCellObject *,然后通過(guò)內(nèi)部的 ob_ref 進(jìn)而獲取符號(hào)對(duì)應(yīng)的值。至于 name 和 age 分別對(duì)應(yīng)哪一個(gè) PyCellObject,這些都體現(xiàn)在字節(jié)碼指令參數(shù)當(dāng)中了。 然后我們?cè)賮?lái)看看 free 變量是如何加載的,它由 LOAD_DEREF 指令完成。
這里再補(bǔ)充一點(diǎn),我們說(shuō) localplus 是一個(gè)連續(xù)的數(shù)組,只是按照用途被劃分成了四個(gè)區(qū)域:保存局部變量的內(nèi)存空間、保存 cell 變量的內(nèi)存空間、保存 free 變量的內(nèi)存空間、運(yùn)行時(shí)棧。 但對(duì)于當(dāng)前的內(nèi)層函數(shù) inner 來(lái)說(shuō),它是沒(méi)有局部變量和 cell 變量的,所以 localsplus 開(kāi)始的位置便是 free 區(qū)域。 當(dāng)然不管是局部變量、cell 變量,還是 free 變量,它們都按照順序保存在 localplus 中,并且在編譯階段便知道它們?cè)?localsplus 中的位置。比如我們將內(nèi)層函數(shù) inner 的邏輯修改一下。 在 inner 里面創(chuàng)建了三個(gè)局部變量,那么它的字節(jié)碼會(huì)變成什么樣子呢?這里我們直接看 print 函數(shù)執(zhí)行時(shí)的字節(jié)碼即可。 因?yàn)?inner 里面沒(méi)有函數(shù)了,所以它不存在 cell 變量,里面只有局部變量和 free 變量。 所以雖然我們說(shuō) localplus 被分成了四份,但是 cell 區(qū)域和 free 區(qū)域很少會(huì)同時(shí)存在。對(duì)于外層函數(shù) some_func 來(lái)說(shuō),它沒(méi)有 free 變量,所以 free 區(qū)域長(zhǎng)度為 0。而對(duì)于內(nèi)層函數(shù) inner 來(lái)說(shuō),它沒(méi)有 cell 變量,所以 cell 區(qū)域長(zhǎng)度為 0。 只有函數(shù)的里面存在內(nèi)層函數(shù),并且外面存在外層函數(shù),那么它才有可能同時(shí)包含 cell 變量和 free 變量。 但為了方便描述,我們?nèi)匀徽J(rèn)為 localplus 被分成了四個(gè)區(qū)域,只不過(guò)對(duì)于外層函數(shù) some_func 而言,它的 free 區(qū)域長(zhǎng)度為 0;對(duì)于 inner 函數(shù)而言,它的 cell 區(qū)域長(zhǎng)度為 0。 當(dāng)然這些都是概念上的東西,大家理解就好。但不管在概念上 localplus 怎么劃分,它本質(zhì)上就是一個(gè) C 數(shù)組,是一段連續(xù)的內(nèi)存,用于存儲(chǔ)局部變量、cell 變量、free 變量(這三種變量不一定同時(shí)存在),以及作為運(yùn)行時(shí)棧。 最重要的是,這三種變量都是基于數(shù)組實(shí)現(xiàn)的靜態(tài)訪問(wèn),并且怎么訪問(wèn)在編譯階段就已經(jīng)確定,因?yàn)樵L問(wèn)數(shù)組的索引會(huì)作為指令參數(shù)存儲(chǔ)在字節(jié)碼指令集中。
這便是靜態(tài)訪問(wèn)。 小結(jié) 本篇文章我們就介紹了閉包,比想象中的要更加簡(jiǎn)單。因?yàn)殚]包仍是一個(gè)函數(shù),只是將外層作用域的局部變量變成了 cell 變量,然后保存在內(nèi)部的 func_closure 字段中。 然后執(zhí)行內(nèi)層函數(shù)的時(shí)候,再將 func_closure 里的 PyCellObject * 拷貝到 localplus 的 free 區(qū)域,此時(shí)我們叫它 free 變量。但不管什么變量,虛擬機(jī)在編譯時(shí)便知道應(yīng)該如何訪問(wèn)指定的內(nèi)存。 |
|
來(lái)自: 古明地覺(jué)O_o > 《待分類》