深入理解python虛擬機如何實現(xiàn)閉包
在本篇文章當中主要從虛擬機層面討論函數(shù)閉包是如何實現(xiàn)的,當能夠從設(shè)計者的層面去理解閉包就再也不用死記硬背一些閉包的概念了,因為如果你理解閉包的設(shè)計原理之后,這些都是非常自然的。
根據(jù) wiki 的描述,a closure is a record storing a function together with an environment。所謂閉包就是將函數(shù)和環(huán)境存儲在一起的記錄。這里有三個重點一個是函數(shù),一個是環(huán)境(簡單說來就是程序當中變量),最后一個需要將兩者組合在一起所形成的東西,才叫做閉包。
Python 中的閉包
我們現(xiàn)在用一種更加直觀的方式描述一下閉包:閉包是指在函數(shù)內(nèi)部定義的函數(shù),它可以訪問外部函數(shù)的局部變量,并且可以在外部函數(shù)執(zhí)行完后繼續(xù)使用這些變量。這是因為閉包在創(chuàng)建時會捕獲其所在作用域的變量,然后保持對這些變量的引用。下面是一個詳細的Python閉包示例:
def?outer_function(x): ????#?外部函數(shù)定義了一個局部變量?x ????def?inner_function(y): ????????#?內(nèi)部函數(shù)可以訪問外部函數(shù)的局部變量?x ????????return?x?+?y ????#?外部函數(shù)返回內(nèi)部函數(shù)的引用,形成閉包 ????return?inner_function #?創(chuàng)建兩個閉包實例,分別使用不同的?x?值 closure1?=?outer_function(10) closure2?=?outer_function(20) #?調(diào)用閉包,它們?nèi)匀豢梢栽L問其所在外部函數(shù)的?x?變量 result1?=?closure1(5)??#?計算?10?+?5,結(jié)果是?15 result2?=?closure2(5)??#?計算?20?+?5,結(jié)果是?25 print(result1) print(result2)
在上面的示例中,outer_function 是外部函數(shù),它接受一個參數(shù) x,然后定義了一個內(nèi)部函數(shù) inner_function,它接受另一個參數(shù) y,并返回 x + y 的結(jié)果。當我們調(diào)用 outer_function 時,它返回了一個對 inner_function 的引用,形成了一個閉包。這個閉包可以保持對 x 的引用,即使 outer_function 已經(jīng)執(zhí)行完畢。
在上面的例子當中 outer_function 的返回值就是閉包,這個閉包包含函數(shù)和環(huán)境,函數(shù)是 inner_function ,環(huán)境就是 x,從程序語義的層面來說返回值是一個閉包,但是如果直接從 Python 層面來看,返回值也是一個函數(shù),現(xiàn)在我們打印兩個閉包看一下結(jié)果:
>>> print(closure1)
<function outer_function.<locals>.inner_function at 0x102e17a60>
>>> print(closure2)
<function outer_function.<locals>.inner_function at 0x1168bc430>
從上面的輸出結(jié)果可以看到兩個閉包(從 Python 層面來說也是函數(shù))所在的內(nèi)存地址是不一樣的,因此每次調(diào)用都會返回一個不同的函數(shù)(閉包),因此兩個閉包相互不影響。
再來看下面的程序,他們的執(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 分別是兩個不同的閉包,兩個閉包的 x 也是各自的 x ,因此前一個閉包的 x 變化并不會影響第二個閉包,所以 result2 的輸出結(jié)果為 26。
閉包相關(guān)的字節(jié)碼
在正式了解閉包相關(guān)的字節(jié)碼之前我們首先來重新回顧一下 CodeObject 當中的字段:
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ù)當中會使用本地定義的變量,freevars 表示本地會使用其他函數(shù)定義的變量。在上面的例子當中,outer_function 當中的變量 x 會被 inner_function 使用,而cellvars 表示在其他函數(shù)當中會使用本地定義的變量,所以 outer_function 的這個字段為 ('x', )。
上面的內(nèi)容我們簡要回顧了一下 CodeObject 當中的兩個非常重要的字段,這兩個字段在進行傳遞參數(shù)的時候非常重要,當我們在進行函數(shù)調(diào)用的時候,虛擬機會新建一個棧幀,在進行新建棧幀的過程當中,如果發(fā)現(xiàn) co_cellvars 存儲的字符串變量也是函數(shù)參數(shù)的時候,除了會在局部變量當中保存一份參數(shù)之外,還會將傳遞過來的參數(shù)保存到棧幀對象的其他位置當中(這里需要注意一下,CodeObject 當中的 co_freevars 保存的是字符串,也就是變量名,棧幀當中保存的是變量名字對應(yīng)的真實對象,也就是函數(shù)參數(shù)),這么做的目的是為了方面后面字節(jié)碼 LOAD_CLOSURE 的操作,因為實際虛擬機存儲的是指向?qū)ο蟮闹羔?,因此浪費不了多少空間。
實際在虛擬機的棧幀對象當中 freevars 是一個數(shù)組,后續(xù)的字節(jié)碼都是會根據(jù)數(shù)組下標對這些變量進行操作。
下面我們分析一下和閉包相關(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 對應(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)在來詳細解釋一下上面的字節(jié)碼含義:
LOAD_CLOSURE:這個就是從棧幀對象當中加載指定下標的 cellvars 變量,在上面的字節(jié)碼當中就是加載棧幀對象 cellvars 當中下標為 0 的對象,對應(yīng)的參數(shù)就是 x 。也就是將參數(shù) x 加載到棧幀上。
BUILD_TUPLE:從棧幀當中彈出 oparg (字節(jié)碼參數(shù)) 個參數(shù),并且將這些參數(shù)封裝成元祖,在上面的程序當中 oparg = 1 。
LOAD_CONST:加載對應(yīng)的常量到棧幀當中,這里是會加載兩個常量,分別是函數(shù)對應(yīng)的 CodeObject 和函數(shù)名。
在執(zhí)行完上的字節(jié)碼之后棧幀當中 valuestack 如下所示:

MAKE_FUNCTION:這條字節(jié)碼的主要作用是根據(jù)上面三個棧里面的對象創(chuàng)建一個函數(shù),其中最重要的字段就是 CodeObject 這里面保存了函數(shù)最重要的代碼,最下面的元祖就是 inner_function 的 freevars,當虛擬機在創(chuàng)建函數(shù)的時候就已經(jīng)把這個對象保存下來了,然后在創(chuàng)建棧幀的時候會將這個對象保存到棧幀。需要注意的是這里所保存的變量就是函數(shù)參數(shù) x,他們是同一個對象。這就使得內(nèi)部函數(shù)每次調(diào)用的時候都可以使用參數(shù) x 。
我們再來看一下函數(shù) inner_function 的字節(jié)碼
- LOAD_DEREF:這個字節(jié)碼會從棧幀的 freevars 數(shù)組當中加載下標為 oparg 的對象,freevars 就是剛剛在創(chuàng)建函數(shù)的時候所保存的,也就是 outter_function 傳遞給 inner_function 的元祖。直觀的來說就是將外部函數(shù)的 x 加載到 valuestack 當中。
- STORE_DEREF:就是將棧頂?shù)脑貜棾?,保存?cellvars 數(shù)組對應(yīng)的下標 (oparg) 當中。
后續(xù)的字節(jié)碼就很簡單了,這里不做詳細分析了。
如果上面的過程太復(fù)雜,我們在這里從整體的角度再敘述一下,簡單說來就是當有代碼調(diào)用 outer_function 的時候,傳遞進來的參數(shù),會在 outer_function 創(chuàng)建函數(shù) inner_function 的時候當作閉包參數(shù)傳遞給 inner_function,這樣 inner_function 就能夠使用 outer_function 的參數(shù)了,因此這也不難理解,每次我們調(diào)用函數(shù) outer_function 都會返回一個新的閉包(實際就是返回的新創(chuàng)建的函數(shù)),因為我們每次調(diào)用函數(shù) outer_function 時,它都會創(chuàng)建一個新的函數(shù),而這些被創(chuàng)建的函數(shù)唯一的區(qū)別就是他們的閉包參數(shù)不同。這也就解釋了再之前的例子當中為什么兩個閉包他們互不影響,因為函數(shù) outer_function 創(chuàng)建了兩個不同的函數(shù)。
總結(jié)
在本篇文章當中詳細介紹了閉包的使用例子和使用原理,理解閉包最重要的一點就是函數(shù)和環(huán)境,也就是和函數(shù)綁定在一起的變量。當進行函數(shù)調(diào)用的時候函數(shù)就會創(chuàng)建一個新的內(nèi)部函數(shù),也就是閉包。在虛擬機內(nèi)部實現(xiàn)閉包主要是通過函數(shù)參數(shù)傳遞和函數(shù)生成實現(xiàn)的,當執(zhí)行 MAKE_FUNCTION 創(chuàng)建新函數(shù)的時候,會將外部函數(shù)的閉包變量 (在文章中就是 x ) 傳遞給內(nèi)部函數(shù),然后保存在內(nèi)部函數(shù)當中,之后的每一次調(diào)用都是用這個變量,從而實現(xiàn)閉包的效果。
到此這篇關(guān)于深入理解python虛擬機如何實現(xiàn)閉包的文章就介紹到這了,更多相關(guān)python虛擬機內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python實現(xiàn)字符串中字符分類及個數(shù)統(tǒng)計
這篇文章主要介紹了python實現(xiàn)字符串中字符分類及個數(shù)統(tǒng)計,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-09-09
Python web如何在IIS發(fā)布應(yīng)用過程解析
這篇文章主要介紹了Python web如何在IIS發(fā)布應(yīng)用過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-05-05
對python同一個文件夾里面不同.py文件的交叉引用方法詳解
今天小編就為大家分享一篇對python同一個文件夾里面不同.py文件的交叉引用方法詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-12-12
Python matplotlib實現(xiàn)散點圖的繪制
Matplotlib作為Python的2D繪圖庫,它以各種硬拷貝格式和跨平臺的交互式環(huán)境生成出版質(zhì)量級別的圖形。本文將利用Matplotlib庫繪制散點圖,感興趣的可以了解一下2022-03-03
Python爬蟲教程之利用正則表達式匹配網(wǎng)頁內(nèi)容
這篇文章主要給大家介紹了關(guān)于Python爬蟲教程之利用正則表達式匹配網(wǎng)頁內(nèi)容的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-12-12

