深入理解python虛擬機如何實現(xiàn)閉包
在本篇文章當中主要從虛擬機層面討論函數(shù)閉包是如何實現(xiàn)的,當能夠從設計者的層面去理解閉包就再也不用死記硬背一些閉包的概念了,因為如果你理解閉包的設計原理之后,這些都是非常自然的。
根據(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,結果是?15 result2?=?closure2(5)??#?計算?20?+?5,結果是?25 print(result1) print(result2)
在上面的示例中,outer_function 是外部函數(shù),它接受一個參數(shù) x,然后定義了一個內(nèi)部函數(shù) inner_function,它接受另一個參數(shù) y,并返回 x + y 的結果。當我們調(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)在我們打印兩個閉包看一下結果:
>>> print(closure1)
<function outer_function.<locals>.inner_function at 0x102e17a60>
>>> print(closure2)
<function outer_function.<locals>.inner_function at 0x1168bc430>
從上面的輸出結果可以看到兩個閉包(從 Python 層面來說也是函數(shù))所在的內(nèi)存地址是不一樣的,因此每次調(diào)用都會返回一個不同的函數(shù)(閉包),因此兩個閉包相互不影響。
再來看下面的程序,他們的執(zhí)行結果是什么?
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)
輸出結果為:
16
17
26
根據(jù)上面的分析 closure1 和 closure2 分別是兩個不同的閉包,兩個閉包的 x 也是各自的 x ,因此前一個閉包的 x 變化并不會影響第二個閉包,所以 result2 的輸出結果為 26。
閉包相關的字節(jié)碼
在正式了解閉包相關的字節(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 保存的是字符串,也就是變量名,棧幀當中保存的是變量名字對應的真實對象,也就是函數(shù)參數(shù)),這么做的目的是為了方面后面字節(jié)碼 LOAD_CLOSURE 的操作,因為實際虛擬機存儲的是指向對象的指針,因此浪費不了多少空間。
實際在虛擬機的棧幀對象當中 freevars 是一個數(shù)組,后續(xù)的字節(jié)碼都是會根據(jù)數(shù)組下標對這些變量進行操作。
下面我們分析一下和閉包相關的字節(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 對應的字節(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 的對象,對應的參數(shù)就是 x 。也就是將參數(shù) x 加載到棧幀上。
BUILD_TUPLE:從棧幀當中彈出 oparg (字節(jié)碼參數(shù)) 個參數(shù),并且將這些參數(shù)封裝成元祖,在上面的程序當中 oparg = 1 。
LOAD_CONST:加載對應的常量到棧幀當中,這里是會加載兩個常量,分別是函數(shù)對應的 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ù)組對應的下標 (oparg) 當中。
后續(xù)的字節(jié)碼就很簡單了,這里不做詳細分析了。
如果上面的過程太復雜,我們在這里從整體的角度再敘述一下,簡單說來就是當有代碼調(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ù)。
總結
在本篇文章當中詳細介紹了閉包的使用例子和使用原理,理解閉包最重要的一點就是函數(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)閉包的效果。
到此這篇關于深入理解python虛擬機如何實現(xiàn)閉包的文章就介紹到這了,更多相關python虛擬機內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
手把手教你部署AI小說生成器(Python環(huán)境搭建)
本文詳細介紹了如何使用Python+AI從零開始搭建屬于自己的AI小說生成器,文章分為環(huán)境準備、第一次安裝、關機重啟、配置大腦等步驟進行講解,幫助用戶輕松實現(xiàn)AI輔助創(chuàng)作2026-04-04
Python實戰(zhàn)之生成有關聯(lián)單選問卷
這篇文章主要為大家分享了一個Python實戰(zhàn)小案例——生成有關聯(lián)單選問卷,并且能根據(jù)問卷總分數(shù)生成對應判斷文案結果,感興趣的可以了解一下2023-04-04
pytorch LayerNorm參數(shù)的用法及計算過程
這篇文章主要介紹了pytorch LayerNorm參數(shù)的用法及計算過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-05-05
Python的Flask框架中使用Flask-SQLAlchemy管理數(shù)據(jù)庫的教程
在Python中我們可以使用SQLAlchemy框架進行數(shù)據(jù)庫操作,那么對應的在Flask框架中我們可以使用SQLAlchemy,下面我們就來看一下Python的Flask框架中使用Flask-SQLAlchemy管理數(shù)據(jù)庫的教程2016-06-06

