深入理解python虛擬機(jī)如何實(shí)現(xiàn)閉包
在本篇文章當(dāng)中主要從虛擬機(jī)層面討論函數(shù)閉包是如何實(shí)現(xiàn)的,當(dāng)能夠從設(shè)計(jì)者的層面去理解閉包就再也不用死記硬背一些閉包的概念了,因?yàn)槿绻憷斫忾]包的設(shè)計(jì)原理之后,這些都是非常自然的。
根據(jù) wiki 的描述,a closure is a record storing a function together with an environment。所謂閉包就是將函數(shù)和環(huán)境存儲(chǔ)在一起的記錄。這里有三個(gè)重點(diǎn)一個(gè)是函數(shù),一個(gè)是環(huán)境(簡(jiǎn)單說來就是程序當(dāng)中變量),最后一個(gè)需要將兩者組合在一起所形成的東西,才叫做閉包。
Python 中的閉包
我們現(xiàn)在用一種更加直觀的方式描述一下閉包:閉包是指在函數(shù)內(nèi)部定義的函數(shù),它可以訪問外部函數(shù)的局部變量,并且可以在外部函數(shù)執(zhí)行完后繼續(xù)使用這些變量。這是因?yàn)殚]包在創(chuàng)建時(shí)會(huì)捕獲其所在作用域的變量,然后保持對(duì)這些變量的引用。下面是一個(gè)詳細(xì)的Python閉包示例:
def?outer_function(x): ????#?外部函數(shù)定義了一個(gè)局部變量?x ????def?inner_function(y): ????????#?內(nèi)部函數(shù)可以訪問外部函數(shù)的局部變量?x ????????return?x?+?y ????#?外部函數(shù)返回內(nèi)部函數(shù)的引用,形成閉包 ????return?inner_function #?創(chuàng)建兩個(gè)閉包實(shí)例,分別使用不同的?x?值 closure1?=?outer_function(10) closure2?=?outer_function(20) #?調(diào)用閉包,它們?nèi)匀豢梢栽L問其所在外部函數(shù)的?x?變量 result1?=?closure1(5)??#?計(jì)算?10?+?5,結(jié)果是?15 result2?=?closure2(5)??#?計(jì)算?20?+?5,結(jié)果是?25 print(result1) print(result2)
在上面的示例中,outer_function
是外部函數(shù),它接受一個(gè)參數(shù) x
,然后定義了一個(gè)內(nèi)部函數(shù) inner_function
,它接受另一個(gè)參數(shù) y
,并返回 x + y
的結(jié)果。當(dāng)我們調(diào)用 outer_function
時(shí),它返回了一個(gè)對(duì) inner_function
的引用,形成了一個(gè)閉包。這個(gè)閉包可以保持對(duì) x
的引用,即使 outer_function
已經(jīng)執(zhí)行完畢。
在上面的例子當(dāng)中 outer_function
的返回值就是閉包,這個(gè)閉包包含函數(shù)和環(huán)境,函數(shù)是 inner_function
,環(huán)境就是 x
,從程序語義的層面來說返回值是一個(gè)閉包,但是如果直接從 Python 層面來看,返回值也是一個(gè)函數(shù),現(xiàn)在我們打印兩個(gè)閉包看一下結(jié)果:
>>> print(closure1)
<function outer_function.<locals>.inner_function at 0x102e17a60>
>>> print(closure2)
<function outer_function.<locals>.inner_function at 0x1168bc430>
從上面的輸出結(jié)果可以看到兩個(gè)閉包(從 Python 層面來說也是函數(shù))所在的內(nèi)存地址是不一樣的,因此每次調(diào)用都會(huì)返回一個(gè)不同的函數(shù)(閉包),因此兩個(gè)閉包相互不影響。
再來看下面的程序,他們的執(zhí)行結(jié)果是什么?
def?outer_function(x): ?def?inner_function(y): ??nonlocal?x ??x?+=?1 ??return?x?+?y ?return?inner_function closure1?=?outer_function(10) closure2?=?outer_function(20) result1?=?closure1(5) print(result1) result1?=?closure1(5) print(result1) result2?=?closure2(5) print(result2)
輸出結(jié)果為:
16
17
26
根據(jù)上面的分析 closure1 和 closure2 分別是兩個(gè)不同的閉包,兩個(gè)閉包的 x 也是各自的 x ,因此前一個(gè)閉包的 x 變化并不會(huì)影響第二個(gè)閉包,所以 result2 的輸出結(jié)果為 26。
閉包相關(guān)的字節(jié)碼
在正式了解閉包相關(guān)的字節(jié)碼之前我們首先來重新回顧一下 CodeObject 當(dāng)中的字段:
def?outer_function(x): ?def?inner_function(y): ??nonlocal?x ??x?+=?1 ??return?x?+?y ?print(inner_function.__code__.co_freevars)??#?('x',) ?print(inner_function.__code__.co_cellvars)??#?() ?return?inner_function if?__name__?==?'__main__': ?out?=?outer_function(1) ?print(outer_function.__code__.co_freevars)??#?() ?print(outer_function.__code__.co_cellvars)??#?('x',?)
cellvars 表示在其他函數(shù)當(dāng)中會(huì)使用本地定義的變量,freevars 表示本地會(huì)使用其他函數(shù)定義的變量。在上面的例子當(dāng)中,outer_function 當(dāng)中的變量 x 會(huì)被 inner_function 使用,而cellvars 表示在其他函數(shù)當(dāng)中會(huì)使用本地定義的變量,所以 outer_function 的這個(gè)字段為 ('x', )。
上面的內(nèi)容我們簡(jiǎn)要回顧了一下 CodeObject 當(dāng)中的兩個(gè)非常重要的字段,這兩個(gè)字段在進(jìn)行傳遞參數(shù)的時(shí)候非常重要,當(dāng)我們?cè)谶M(jìn)行函數(shù)調(diào)用的時(shí)候,虛擬機(jī)會(huì)新建一個(gè)棧幀,在進(jìn)行新建棧幀的過程當(dāng)中,如果發(fā)現(xiàn) co_cellvars 存儲(chǔ)的字符串變量也是函數(shù)參數(shù)的時(shí)候,除了會(huì)在局部變量當(dāng)中保存一份參數(shù)之外,還會(huì)將傳遞過來的參數(shù)保存到棧幀對(duì)象的其他位置當(dāng)中(這里需要注意一下,CodeObject 當(dāng)中的 co_freevars 保存的是字符串,也就是變量名,棧幀當(dāng)中保存的是變量名字對(duì)應(yīng)的真實(shí)對(duì)象,也就是函數(shù)參數(shù)),這么做的目的是為了方面后面字節(jié)碼 LOAD_CLOSURE 的操作,因?yàn)閷?shí)際虛擬機(jī)存儲(chǔ)的是指向?qū)ο蟮闹羔?,因此浪費(fèi)不了多少空間。
實(shí)際在虛擬機(jī)的棧幀對(duì)象當(dāng)中 freevars 是一個(gè)數(shù)組,后續(xù)的字節(jié)碼都是會(huì)根據(jù)數(shù)組下標(biāo)對(duì)這些變量進(jìn)行操作。
下面我們分析一下和閉包相關(guān)的字節(jié)碼操作
def?outer_function(x): ?def?inner_function(y): ??nonlocal?x ??x?+=?1 ??return?x?+?y ?return?inner_function if?__name__?==?'__main__': ?import?dis ?dis.dis(outer_function)
上面的代碼回輸出 outer_function 和 inner_function 對(duì)應(yīng)的字節(jié)碼:
2 0 LOAD_CLOSURE 0 (x)
2 BUILD_TUPLE 1
4 LOAD_CONST 1 (<code object inner_function at 0x100757a80, file "closure_bytecode.py", line 2>)
6 LOAD_CONST 2 ('outer_function.<locals>.inner_function')
8 MAKE_FUNCTION 8 (closure)
10 STORE_FAST 1 (inner_function)
7 12 LOAD_FAST 1 (inner_function)
14 RETURN_VALUE
Disassembly of <code object inner_function at 0x100757a80, file "closure_bytecode.py", line 2>:
4 0 LOAD_DEREF 0 (x)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_DEREF 0 (x)
5 8 LOAD_DEREF 0 (x)
10 LOAD_FAST 0 (y)
12 BINARY_ADD
14 RETURN_VALUE
我們現(xiàn)在來詳細(xì)解釋一下上面的字節(jié)碼含義:
LOAD_CLOSURE:這個(gè)就是從棧幀對(duì)象當(dāng)中加載指定下標(biāo)的 cellvars 變量,在上面的字節(jié)碼當(dāng)中就是加載棧幀對(duì)象 cellvars 當(dāng)中下標(biāo)為 0 的對(duì)象,對(duì)應(yīng)的參數(shù)就是 x 。也就是將參數(shù) x 加載到棧幀上。
BUILD_TUPLE:從棧幀當(dāng)中彈出 oparg (字節(jié)碼參數(shù)) 個(gè)參數(shù),并且將這些參數(shù)封裝成元祖,在上面的程序當(dāng)中 oparg = 1 。
LOAD_CONST:加載對(duì)應(yīng)的常量到棧幀當(dāng)中,這里是會(huì)加載兩個(gè)常量,分別是函數(shù)對(duì)應(yīng)的 CodeObject 和函數(shù)名。
在執(zhí)行完上的字節(jié)碼之后棧幀當(dāng)中 valuestack 如下所示:
MAKE_FUNCTION:這條字節(jié)碼的主要作用是根據(jù)上面三個(gè)棧里面的對(duì)象創(chuàng)建一個(gè)函數(shù),其中最重要的字段就是 CodeObject 這里面保存了函數(shù)最重要的代碼,最下面的元祖就是 inner_function 的 freevars,當(dāng)虛擬機(jī)在創(chuàng)建函數(shù)的時(shí)候就已經(jīng)把這個(gè)對(duì)象保存下來了,然后在創(chuàng)建棧幀的時(shí)候會(huì)將這個(gè)對(duì)象保存到棧幀。需要注意的是這里所保存的變量就是函數(shù)參數(shù) x,他們是同一個(gè)對(duì)象。這就使得內(nèi)部函數(shù)每次調(diào)用的時(shí)候都可以使用參數(shù) x 。
我們?cè)賮砜匆幌潞瘮?shù) inner_function 的字節(jié)碼
- LOAD_DEREF:這個(gè)字節(jié)碼會(huì)從棧幀的 freevars 數(shù)組當(dāng)中加載下標(biāo)為 oparg 的對(duì)象,freevars 就是剛剛在創(chuàng)建函數(shù)的時(shí)候所保存的,也就是 outter_function 傳遞給 inner_function 的元祖。直觀的來說就是將外部函數(shù)的 x 加載到 valuestack 當(dāng)中。
- STORE_DEREF:就是將棧頂?shù)脑貜棾?,保存?cellvars 數(shù)組對(duì)應(yīng)的下標(biāo) (oparg) 當(dāng)中。
后續(xù)的字節(jié)碼就很簡(jiǎn)單了,這里不做詳細(xì)分析了。
如果上面的過程太復(fù)雜,我們?cè)谶@里從整體的角度再敘述一下,簡(jiǎn)單說來就是當(dāng)有代碼調(diào)用 outer_function 的時(shí)候,傳遞進(jìn)來的參數(shù),會(huì)在 outer_function 創(chuàng)建函數(shù) inner_function 的時(shí)候當(dāng)作閉包參數(shù)傳遞給 inner_function,這樣 inner_function 就能夠使用 outer_function 的參數(shù)了,因此這也不難理解,每次我們調(diào)用函數(shù) outer_function 都會(huì)返回一個(gè)新的閉包(實(shí)際就是返回的新創(chuàng)建的函數(shù)),因?yàn)槲覀兠看握{(diào)用函數(shù) outer_function 時(shí),它都會(huì)創(chuàng)建一個(gè)新的函數(shù),而這些被創(chuàng)建的函數(shù)唯一的區(qū)別就是他們的閉包參數(shù)不同。這也就解釋了再之前的例子當(dāng)中為什么兩個(gè)閉包他們互不影響,因?yàn)楹瘮?shù) outer_function 創(chuàng)建了兩個(gè)不同的函數(shù)。
總結(jié)
在本篇文章當(dāng)中詳細(xì)介紹了閉包的使用例子和使用原理,理解閉包最重要的一點(diǎn)就是函數(shù)和環(huán)境,也就是和函數(shù)綁定在一起的變量。當(dāng)進(jìn)行函數(shù)調(diào)用的時(shí)候函數(shù)就會(huì)創(chuàng)建一個(gè)新的內(nèi)部函數(shù),也就是閉包。在虛擬機(jī)內(nèi)部實(shí)現(xiàn)閉包主要是通過函數(shù)參數(shù)傳遞和函數(shù)生成實(shí)現(xiàn)的,當(dāng)執(zhí)行 MAKE_FUNCTION 創(chuàng)建新函數(shù)的時(shí)候,會(huì)將外部函數(shù)的閉包變量 (在文章中就是 x ) 傳遞給內(nèi)部函數(shù),然后保存在內(nèi)部函數(shù)當(dāng)中,之后的每一次調(diào)用都是用這個(gè)變量,從而實(shí)現(xiàn)閉包的效果。
到此這篇關(guān)于深入理解python虛擬機(jī)如何實(shí)現(xiàn)閉包的文章就介紹到這了,更多相關(guān)python虛擬機(jī)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python實(shí)現(xiàn)字符串中字符分類及個(gè)數(shù)統(tǒng)計(jì)
這篇文章主要介紹了python實(shí)現(xiàn)字符串中字符分類及個(gè)數(shù)統(tǒng)計(jì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09Python web如何在IIS發(fā)布應(yīng)用過程解析
這篇文章主要介紹了Python web如何在IIS發(fā)布應(yīng)用過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05對(duì)python同一個(gè)文件夾里面不同.py文件的交叉引用方法詳解
今天小編就為大家分享一篇對(duì)python同一個(gè)文件夾里面不同.py文件的交叉引用方法詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-12-12Python matplotlib實(shí)現(xiàn)散點(diǎn)圖的繪制
Matplotlib作為Python的2D繪圖庫(kù),它以各種硬拷貝格式和跨平臺(tái)的交互式環(huán)境生成出版質(zhì)量級(jí)別的圖形。本文將利用Matplotlib庫(kù)繪制散點(diǎn)圖,感興趣的可以了解一下2022-03-03Python爬蟲教程之利用正則表達(dá)式匹配網(wǎng)頁內(nèi)容
這篇文章主要給大家介紹了關(guān)于Python爬蟲教程之利用正則表達(dá)式匹配網(wǎng)頁內(nèi)容的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12