閉包是怎么實現(xiàn)的?你知道嗎?
楔子
在之前的文章中一直反復(fù)提到四個字:名字空間。一段代碼執(zhí)行的結(jié)果不光取決于代碼中的符號,更多的是取決于代碼中符號的語義,而這個運行時的語義正是由名字空間決定的。
名字空間是由虛擬機在運行時動態(tài)維護的,但有時我們希望將名字空間靜態(tài)化。換句話說,我們希望有的代碼不受名字空間變化帶來的影響,始終保持一致的功能該怎么辦呢?隨便舉個例子:
def login(user_name, password, user):
if not (user_name == "satori" and password == "123"):
return "用戶名密碼不正確"
else:
return f"歡迎: {user}"
print(login("satori", "123", "古明地覺")) # 歡迎: 古明地覺
print(login("satori", "123", "古明地戀")) # 歡迎: 古明地戀
我們注意到每次都需要輸入 username 和 password,于是可以通過使用嵌套函數(shù)來設(shè)置一個基準(zhǔn)值。
def deco(user_name, password):
def login(user):
if not (user_name == "satori" and password == "123"):
return "用戶名密碼不正確"
else:
return f"歡迎: {user}"
return login
login = deco("satori", "123")
print(login("古明地覺")) # 歡迎: 古明地覺
print(login("古明地戀")) # 歡迎: 古明地戀
盡管函數(shù) login 里面沒有 user_name 和 password 這兩個局部變量,但是不妨礙我們使用它,因為外層函數(shù) deco 里面有。
也就是說,函數(shù) login 作為函數(shù) deco 的返回值被返回的時候,有一個名字空間就已經(jīng)和 login 緊緊地綁定在一起了。執(zhí)行內(nèi)層函數(shù) login 的時候,對于自身 local 空間中不存在的變量,會從和自己綁定的 local 空間里面去找,這就是一種將名字空間靜態(tài)化的方法。這個名字空間和內(nèi)層函數(shù)捆綁之后的結(jié)果我們稱之為閉包(closure)。
為了描述方便,上面說的是 local 空間,但我們知道,局部變量不是從那里查找的,而是從 localsplus 里面。只是我們可以按照 LEGB 的規(guī)則去理解,這一點心理清楚就行。
也就是說:閉包=外部作用域+內(nèi)層函數(shù)。并且在介紹函數(shù)的時候提到,PyFunctionObject 是虛擬機專門為字節(jié)碼指令的傳輸而準(zhǔn)備的大包袱,global 名字空間、默認參數(shù)都和字節(jié)碼指令捆綁在一起,同樣的,也包括閉包。
實現(xiàn)閉包的基石
閉包的創(chuàng)建通常是利用嵌套函數(shù)來完成的,我們說過局部變量是通過數(shù)組靜態(tài)存儲的,而閉包也是如此。這里再來回顧一下 PyCodeObject 里面的幾個關(guān)鍵字段:
- co_localsplusnames:包含所有局部變量、cell 變量、free 變量的名稱
- co_nlocalsplus:co_localsplusnames 的長度,或者說這些變量的個數(shù)之和
- co_varnames:包含所有局部變量的名稱
- co_nlocals:局部變量的個數(shù)
- co_cellvars:包含所有 cell 變量的名稱
- co_ncellvars:cell 變量的個數(shù)
- co_freevars:包含所有 free 變量的名稱
- co_nfreevars:free 變量的個數(shù)
因此不難得出它們之間的關(guān)系:
- co_localsplusnames = co_varnames + co_cellvars + co_freevars
- co_nlocalsplus = co_nlocals + co_ncellvars + co_nfreevars
那么這些變量的值都存在什么地方呢?沒錯就是棧幀的 localsplus 字段中。
圖片
我們看一段代碼:
def foo():
name = "古明地覺"
age = 17
gender = "female"
def bar():
nonlocal name
nonlocal age
print(gender)
return bar
print(foo.__code__.co_cellvars) # ('age', 'gender', 'name')
print(foo().__code__.co_freevars) # ('age', 'gender', 'name')
print(foo.__code__.co_freevars) # ()
print(foo().__code__.co_cellvars) # ()
和閉包相關(guān)的兩個字段是 co_cellvars 和 co_freevars。co_cellvars 保存了外層作用域中被內(nèi)層作用域引用的變量的名字,co_freevars 保存了內(nèi)層作用域中引用的外層作用域的變量的名字。
所以對于外層函數(shù)來說,應(yīng)該使用 co_cellvars,對于內(nèi)層函數(shù)來說,應(yīng)該使用 co_freevars。當(dāng)然無論是外層函數(shù)還是內(nèi)層函數(shù)都有 co_cellvars 和 co_freevars,這是肯定的,因為都是函數(shù)。
只不過外層函數(shù)需要使用 co_cellvars 獲取,因為它包含的是外層函數(shù)中被內(nèi)層函數(shù)引用的變量的名稱;內(nèi)層函數(shù)需要使用 co_freevars 獲取,它包含的是內(nèi)層函數(shù)中引用的外層函數(shù)的變量的名稱。
如果使用外層函數(shù) foo 獲取 co_freevars 的話,那么得到的結(jié)果顯然就是個空元組了,除非 foo 也作為某個函數(shù)的內(nèi)層函數(shù),并且內(nèi)部引用了外層函數(shù)的變量。同理內(nèi)層函數(shù) bar 也是一樣的道理,它獲取 co_cellvars 得到的也是空元組,因為對于 bar 而言不存在內(nèi)層函數(shù)。
我們再看個例子:
def foo():
name = "古明地覺"
age = 17
def bar():
nonlocal name
nonlocal age
gender = "female"
def inner():
nonlocal gender
return inner
return bar
print(foo().__code__.co_cellvars) # ('gender',)
print(foo().__code__.co_freevars) # ('age', 'name')
對于函數(shù) bar 而言,它是函數(shù) inner 的外層函數(shù),同時也是函數(shù) foo 的內(nèi)層函數(shù)。所以它在獲取 co_cellvars 和 co_freevars 屬性時,得到的元組都不為空。因為內(nèi)層函數(shù) inner 引用了函數(shù) bar 里面的變量 gender,同時函數(shù) bar 也作為內(nèi)層函數(shù)引用了函數(shù) foo 里的 name 和 age。
那么問題來了,閉包變量所需要的空間申請在哪個地方呢?沒錯,顯然是 localsplus。
在以前的版本中,這個字段叫 f_localsplus,現(xiàn)在叫 localsplus。
localplus 是一個柔性數(shù)組,它被分成了四份,分別用于:局部變量、cell 變量、free 變量、運行時棧。
所以閉包變量同樣是以靜態(tài)的方式實現(xiàn)的。
閉包的實現(xiàn)過程
介紹完實現(xiàn)閉包的基石之后,我們可以開始追蹤閉包的具體實現(xiàn)過程了,當(dāng)然還是要先看一下閉包對應(yīng)的字節(jié)碼。
import dis
code_string = """
def some_func():
name = "satori"
age = 17
gender = "female"
def inner():
print(name, age)
return inner
func = some_func()
func()
"""
dis.dis(compile(code_string, "<file>", "exec"))
字節(jié)碼指令如下,為了閱讀方便,我們省略了源代碼行號。
# ********** 模塊對應(yīng)的字節(jié)碼 **********
0 RESUME 0
# 加載函數(shù) some_func 對應(yīng)的 PyCodeObject,壓入運行時棧
2 LOAD_CONST 0 (<code object some_func at ...>)
# 從棧頂彈出 PyCodeObject,構(gòu)造 PyFunctionObject,并壓入運行時棧
4 MAKE_FUNCTION 0
# 從棧頂彈出 PyFunctionObject,然后使用變量 some_func 保存
6 STORE_NAME 0 (some_func)
8 PUSH_NULL
# 加載全局變量 some_func
10 LOAD_NAME 0 (some_func)
# 調(diào)用
12 CALL 0
# 彈出棧頂?shù)姆祷刂担⑹褂米兞?func 保存
20 STORE_NAME 1 (func)
22 PUSH_NULL
# 加載全局變量 func
24 LOAD_NAME 1 (func)
# 調(diào)用
26 CALL 0
# 從棧頂彈出返回值,丟棄
34 POP_TOP
# 隱式地 return None
36 RETURN_CONST 1 (None)
# ********** 外層函數(shù) some_func 對應(yīng)的字節(jié)碼 **********
Disassembly of <code object some_func at ...>:
# 創(chuàng)建 cell 對象 PyCellObject,該指令一會兒說
0 MAKE_CELL 2 (age)
2 MAKE_CELL 3 (name)
4 RESUME 0
# 加載常量 "satori"
6 LOAD_CONST 1 ('satori')
# 注意這里不是 STORE_FAST,而是 STORE_DEREF
# 它的作用肯定是將符號 "name" 和字符串常量綁定起來
# STORE_NAME、STORE_FAST、STORE_DEREF 做的事情是一樣的
# 都是將符號和值綁定起來,只是綁定的方式不一樣
# 比如 STORE_NAME 是通過字典完成綁定,STORE_FAST 是通過數(shù)組完成綁定
# 那么 STORE_DEREF 是怎么綁定的呢?稍后分析
8 STORE_DEREF 3 (name)
# 加載常量 17
10 LOAD_CONST 2 (17)
# 使用變量 age 保存
12 STORE_DEREF 2 (age)
# name 和 age 被內(nèi)層函數(shù)引用了,所以是 STORE_DEREF
# 但 gender 沒有,所以它對應(yīng)的是 STORE_FAST
14 LOAD_CONST 3 ('female')
16 STORE_FAST 0 (gender)
# 加載 cell 變量,壓入運行時棧
18 LOAD_CLOSURE 2 (age)
20 LOAD_CLOSURE 3 (name)
# 彈出 cell 變量,構(gòu)建元組
22 BUILD_TUPLE 2
# 加載函數(shù) inner 對應(yīng)的 PyCodeObject
24 LOAD_CONST 4 (<code object inner at ...>)
# 構(gòu)造函數(shù)
26 MAKE_FUNCTION 8 (closure)
# 將函數(shù)使用 inner 變量保存
28 STORE_FAST 1 (inner)
# return inner
30 LOAD_FAST 1 (inner)
32 RETURN_VALUE
# ********** 內(nèi)層函數(shù) inner 對應(yīng)的字節(jié)碼 **********
Disassembly of <code object inner at ...>:
0 COPY_FREE_VARS 2
2 RESUME 0
# 加載內(nèi)置變量 print
4 LOAD_GLOBAL 1 (NULL + print)
# 顯然它和 LOAD_NAME、LOAD_FAST 的關(guān)系也是類似的
# 也是負責(zé)加載變量,然后壓入運行時棧
14 LOAD_DEREF 1 (name)
16 LOAD_DEREF 0 (age)
# 調(diào)用 print 函數(shù)
18 CALL 2
# 從棧頂彈出返回值,丟棄
26 POP_TOP
28 RETURN_CONST 0 (None)
字節(jié)碼的內(nèi)容并不難,我們來分析一下,這里先分析外層函數(shù) some_func 對應(yīng)的字節(jié)碼。
圖片
函數(shù) some_func 里面有三個局部變量,但只有 name 和 age 被內(nèi)層函數(shù)引用了,所以開頭有兩個 MAKE_CELL 指令。參數(shù)為符號在符號表中的索引,對應(yīng)的符號分別為 age 和 name。我們來看一下這個指令是做什么的。
TARGET(MAKE_CELL) {
#line 1394 "Python/bytecodes.c"
// 符號在符號表中的索引,和對應(yīng)的值在 localplus 中的索引是一致的
// 所以這里會獲取變量對應(yīng)的值,對于當(dāng)前來說就是 age 和 name 的值
// 但很明顯此時 name 和 age 還沒有完成賦值,所以結(jié)果為 NULL
PyObject *initial = GETLOCAL(oparg);
// 調(diào)用 PyCell_New 創(chuàng)建 Cell 對象
PyObject *cell = PyCell_New(initial);
if (cell == NULL) {
goto resume_with_error;
}
// 用 Cell 對象替換掉原來的值
SETLOCAL(oparg, cell);
#line 1909 "Python/generated_cases.c.h"
DISPATCH();
}
// Objects/cellobject.c
PyObject *
PyCell_New(PyObject *obj)
{
PyCellObject *op;
// 為 PyCellObject 申請內(nèi)存
op = (PyCellObject *)PyObject_GC_New(PyCellObject, &PyCell_Type);
if (op == NULL)
return NULL;
// 增加 obj 指向?qū)ο蟮囊糜嫈?shù),并賦值給 op->ob_ref
op->ob_ref = Py_XNewRef(obj);
_PyObject_GC_TRACK(op);
return (PyObject *)op;
}
// Include/cpython/cellobject.h
typedef struct {
PyObject_HEAD
PyObject *ob_ref;
} PyCellObject;
所以 MAKE_CELL 指令的作用是創(chuàng)建 PyCellObject,對于當(dāng)前來說,會創(chuàng)建兩個 PyCellObejct,它們的 ob_ref 字段分別為 age 和 name。只不過由于 name 和 age 還尚未完成賦值,所以此時為 NULL。
圖片
接下來就是變量賦值,這個顯然沒什么難度,我們只需要看一下 STORE_DEREF 指令。并且也容易得出結(jié)論,如果局部變量被內(nèi)層函數(shù)所引用,那么指令將不再是 LOAD_FAST 和 STORE_FAST,而是 LOAD_DEREF 和 STORE_DEREF。
TARGET(STORE_DEREF) {
// 由于在 STORE_DEREF 之前調(diào)用了 LOAD_CONST
// 所以這里會獲取上一步壓入的常量,對于當(dāng)前來說就是 17 和 "satori"
PyObject *v = stack_pointer[-1];
#line 1463 "Python/bytecodes.c"
// 這里的 oparg 和 MAKE_CELL 的 oparg 的含義是一樣的
// 此時會拿到對應(yīng)的 PyCellObject *
PyObject *cell = GETLOCAL(oparg);
// 獲取 PyCellObject 內(nèi)部的 ob_ref,并減少引用計數(shù)
PyObject *oldobj = PyCell_GET(cell);
// 將 PyCellObject 內(nèi)部的 ob_ref 設(shè)置成 v
PyCell_SET(cell, v);
Py_XDECREF(oldobj);
#line 1993 "Python/generated_cases.c.h"
// 棧收縮
STACK_SHRINK(1);
DISPATCH();
}
localplus 保存了局部變量的值,而符號在符號表中的索引,和對應(yīng)的值在 localplus 中的索引是一致的。所以正常情況下,局部變量賦值就是 localsplus[oparg] = v。
但在執(zhí)行 MAKE_CELL 指令之后,局部變量賦值就變成了 localsplus[oparg]->ob_ref = v,因為此時 localplus 保存的是 PyCellObject 的地址。
因此在兩個 STORE_DEREF 執(zhí)行完之后,localplus 會變成下面這樣。
相信你明白 STORE_FAST 和 STORE_DEREF 之間的區(qū)別了,如果是 STORE_FAST,那么中間就沒有 PyCellObject 這一層,localsplus 保存的 PyObject * 指向的就是具體的對象。
然后是 gender = "female",它就很簡單了,由于符號 "gender" 在符號表中的索引為 0,那么直接讓 localplus[0] 指向字符串 "female" 即可。
到此變量 name、age、gender 均已賦值完畢,此時 localsplus 結(jié)構(gòu)如下。
圖片
localsplus[0]、localsplus[2]、localsplus[3] 分別對應(yīng)變量 gender、age、name,可能有人覺得,這個索引好奇怪啊,我們實際測試一下。
def some_func():
name = "satori"
age = 17
gender = "female"
def inner():
print(name, age)
return inner
print(
some_func.__code__.co_varnames
) # ('gender', 'inner')
我們看到 some_func 的符號表里面只有 gender 和 inner,因此 localplus[0] 表示變量 gender。至于 localplus[1] 則表示變量 inner,只不過此時它指向的對象還沒有創(chuàng)建,所以暫時為 NULL。
那么問題來了,變量 name 和 age 呢?毫無疑問,由于它們被內(nèi)層函數(shù)引用了,所以它們變成了 cell 變量,并且位置是 co->co_nlocals + i。因為在 localsplus 中,cell 變量的位置是在局部變量之后的,這也完全符合我們之前說的 localsplus 的內(nèi)存布局。
圖片
并且我們看到無論是局部變量還是 cell 變量,都是通過數(shù)組索引訪問的,并且索引在編譯時就確定了,以指令參數(shù)的形式保存在字節(jié)碼指令集中。
接下來執(zhí)行偏移量為 18 和 20 的兩條指令,它們都是 LOAD_CLOSURE。
// 加載 PyCellObject *,即 cell 變量,然后壓入運行時棧
TARGET(LOAD_CLOSURE) {
PyObject *value;
#line 179 "Python/bytecodes.c"
value = GETLOCAL(oparg);
if (value == NULL) goto unbound_local_error;
Py_INCREF(value);
#line 66 "Python/generated_cases.c.h"
STACK_GROW(1);
stack_pointer[-1] = value;
DISPATCH();
}
LOAD_CLOSURE 執(zhí)行完畢后,接著執(zhí)行 BUILD_TUPLE,將 cell 變量從棧中彈出,構(gòu)建元組。然后繼續(xù)執(zhí)行 24 LOAD_CONST,將內(nèi)層函數(shù) inner 對應(yīng)的 PyCodeObject 壓入運行時棧。
接著執(zhí)行 26 MAKE_FUNCTION,將棧中元素彈出,分別是 inner 對應(yīng)的 PyCodeObject 和一個元組,元組里面包含了 inner 使用的外層函數(shù)的變量。當(dāng)然這里的變量已經(jīng)不再是普通的變量了,而是 cell 變量,它內(nèi)部的 ob_ref 字段才是我們需要的。
等元素彈出之后,開始構(gòu)建函數(shù),我們看一下 MAKE_FUNCTION 指令,它的指令參數(shù)為 8。
TARGET(MAKE_FUNCTION) {
PyObject *codeobj = stack_pointer[-1];
PyObject *closure = (oparg & 0x08) ? stack_pointer[...] : NULL;
// ...
// 創(chuàng)建函數(shù)對象
PyFunctionObject *func_obj = (PyFunctionObject *)
PyFunction_New(codeobj, GLOBALS());
Py_DECREF(codeobj);
if (func_obj == NULL) {
goto error;
}
// 由于指令參數(shù)為 8,所以 oparg & 0x08 為真
if (oparg & 0x08) {
assert(PyTuple_CheckExact(closure));
func_obj->func_closure = closure;
}
if (oparg & 0x04) {
assert(PyTuple_CheckExact(annotations));
func_obj->func_annotations = annotations;
}
if (oparg & 0x02) {
assert(PyDict_CheckExact(kwdefaults));
func_obj->func_kwdefaults = kwdefaults;
}
if (oparg & 0x01) {
assert(PyTuple_CheckExact(defaults));
func_obj->func_defaults = defaults;
}
// ...
DISPATCH();
}
所以 PyFunctionObject 再一次承擔(dān)了工具人的角色,創(chuàng)建內(nèi)層函數(shù) inner 時,會將包含 cell 變量的元組賦值給 func_closure 字段。此時便將內(nèi)層函數(shù)需要使用的變量和內(nèi)層函數(shù)綁定在了一起,而這個綁定的結(jié)果我們就稱之為閉包。
但是從結(jié)構(gòu)上來看,閉包仍是一個函數(shù),所謂綁定,其實只是修改了它的 func_closure 字段。當(dāng)函數(shù)創(chuàng)建完畢后,localplus 的結(jié)構(gòu)變化如下。
圖片
函數(shù)即變量,對于函數(shù) some_func 而言,內(nèi)層函數(shù) inner 也是一個局部變量,由于符號 inner 位于符號表中索引為 1 的位置。因此當(dāng)函數(shù)創(chuàng)建完畢時,會修改 localplus[1],讓它保存函數(shù)的地址。不難發(fā)現(xiàn),對于局部變量來說,如何訪問內(nèi)存在編譯階段就確定了。
函數(shù)內(nèi)部的 func_closure 字段指向一個元組,元組里面的每個元素會指向 PyCellObject。
調(diào)用閉包
閉包的創(chuàng)建過程我們已經(jīng)了解了,我們用 Python 代碼再解釋一下。
def some_func():
name = "satori"
age = 17
gender = "female"
def inner():
print(name, age)
return inner
func = some_func()
# some_func 調(diào)用之后會返回內(nèi)層函數(shù) inner
# 只不過 inner 的 func_closure 字段保存了 cell 變量
# 而 cell 變量指向的 PyCellObject 對外層作用域的局部變量進行了凍結(jié)
# 所以我們也會稱呼 inner 函數(shù)為閉包,但要知道閉包仍然是個函數(shù)
print(func.__name__) # inner
print(func.__class__) # <class 'function'>
print(
func.__closure__[0]
) # <cell at 0x102019390: int object at 0x1000b02f0>
print(
func.__closure__[1]
) # <cell at 0x1002126e0: str object at 0x1001f0b70>
print(func.__closure__[0].cell_contents) # 17
print(func.__closure__[1].cell_contents) # satori
調(diào)用 inner 函數(shù)時,外層函數(shù) some_func 已經(jīng)執(zhí)行結(jié)束,但它的局部變量 name 和 age 仍可被內(nèi)層函數(shù) inner 訪問,背后的原因我們算是徹底明白了。
因為 name 和 age 被內(nèi)層函數(shù)引用了,所以虛擬機將它們封裝成了 PyCellObject *,即 cell 變量,而 cell 變量指向的 cell 對象內(nèi)部的 ob_ref 字段對應(yīng)原來的變量。當(dāng)創(chuàng)建內(nèi)層函數(shù)時,將引用的 cell 變量組成元組,保存在內(nèi)層函數(shù)的 func_closure 字段中。
所以當(dāng)內(nèi)層函數(shù)在訪問 name 和 age 時,訪問的其實是 PyCellObject 的 ob_ref 字段。至于變量 name 和 age 對應(yīng)哪一個 PyCellObject,這些在編譯階段便確定了,我們看一下內(nèi)層函數(shù) inner 的字節(jié)碼指令。
圖片
函數(shù)在執(zhí)行時會創(chuàng)建棧幀,我們上面看到的 localsplus 是外層函數(shù) some_func 對應(yīng)的棧幀的 localsplus。而內(nèi)層函數(shù) inner 執(zhí)行時,也會創(chuàng)建棧幀,然后在棧幀中執(zhí)行字節(jié)碼指令。
首先第一個指令是 COPY_FREE_VARS,看一下它的邏輯。
// 將 func_closure 里面的 cell 變量拷貝到 free 區(qū)域
TARGET(COPY_FREE_VARS) {
#line 1470 "Python/bytecodes.c"
/* Copy closure variables to free variables */
PyCodeObject *co = frame->f_code;
assert(PyFunction_Check(frame->f_funcobj));
// 獲取 func_closure,它指向一個元組,里面保存了 PyCellObject *
PyObject *closure = ((PyFunctionObject *)frame->f_funcobj)->func_closure;
assert(oparg == co->co_nfreevars);
// co_nlocalsplus 等于局部變量、cell 變量、free 變量的個數(shù)之和
// 顯然 offset 表示 free 變量對應(yīng)的內(nèi)存區(qū)域
int offset = co->co_nlocalsplus - oparg;
// 將 func_closure 里面的 PyCellObject * 拷貝到 free 區(qū)域
for (int i = 0; i < oparg; ++i) {
PyObject *o = PyTuple_GET_ITEM(closure, i);
frame->localsplus[offset + i] = Py_NewRef(o);
}
#line 2010 "Python/generated_cases.c.h"
DISPATCH();
}
處理完之后,localplus 的布局如下,注意:此時是內(nèi)層函數(shù)對應(yīng)的 localplus。
圖片
在構(gòu)建內(nèi)層函數(shù)時,會將 cell 變量打包成一個元組,交給內(nèi)層函數(shù)的 func_closure 字段。然后執(zhí)行內(nèi)層函數(shù)創(chuàng)建棧幀的時候,再將 func_closure 中的 cell 變量拷貝到 localsplus 的第三段內(nèi)存中。當(dāng)然對于內(nèi)層函數(shù)而言,此時它應(yīng)該叫做 free 變量。
而在調(diào)用內(nèi)層函數(shù) inner 的過程中,當(dāng)引用外層作用域的符號時,一定是到 localsplus 里面的 free 區(qū)域(第三段內(nèi)存)去獲取對應(yīng)的 PyCellObject *,然后通過內(nèi)部的 ob_ref 進而獲取符號對應(yīng)的值。至于 name 和 age 分別對應(yīng)哪一個 PyCellObject,這些都體現(xiàn)在字節(jié)碼指令參數(shù)當(dāng)中了。
然后我們再來看看 free 變量是如何加載的,它由 LOAD_DEREF 指令完成。
TARGET(LOAD_DEREF) {
PyObject *value;
#line 1453 "Python/bytecodes.c"
// 加載 PyCellObject *
PyObject *cell = GETLOCAL(oparg);
// 獲取 PyCellObject 對象的 ob_ref 字段的值
value = PyCell_GET(cell);
if (value == NULL) {
format_exc_unbound(tstate, frame->f_code, oparg);
if (true) goto error;
}
Py_INCREF(value);
#line 1980 "Python/generated_cases.c.h"
STACK_GROW(1);
stack_pointer[-1] = value;
DISPATCH();
}
這里再補充一點,我們說 localplus 是一個連續(xù)的數(shù)組,只是按照用途被劃分成了四個區(qū)域:保存局部變量的內(nèi)存空間、保存 cell 變量的內(nèi)存空間、保存 free 變量的內(nèi)存空間、運行時棧。
但對于當(dāng)前的內(nèi)層函數(shù) inner 來說,它是沒有局部變量和 cell 變量的,所以 localsplus 開始的位置便是 free 區(qū)域。
當(dāng)然不管是局部變量、cell 變量,還是 free 變量,它們都按照順序保存在 localplus 中,并且在編譯階段便知道它們在 localsplus 中的位置。比如我們將內(nèi)層函數(shù) inner 的邏輯修改一下。
圖片
在 inner 里面創(chuàng)建了三個局部變量,那么它的字節(jié)碼會變成什么樣子呢?這里我們直接看 print 函數(shù)執(zhí)行時的字節(jié)碼即可。
圖片
因為 inner 里面沒有函數(shù)了,所以它不存在 cell 變量,里面只有局部變量和 free 變量。
圖片
所以雖然我們說 localplus 被分成了四份,但是 cell 區(qū)域和 free 區(qū)域很少會同時存在。對于外層函數(shù) some_func 來說,它沒有 free 變量,所以 free 區(qū)域長度為 0。而對于內(nèi)層函數(shù) inner 來說,它沒有 cell 變量,所以 cell 區(qū)域長度為 0。
只有函數(shù)的里面存在內(nèi)層函數(shù),并且外面存在外層函數(shù),那么它才有可能同時包含 cell 變量和 free 變量。
但為了方便描述,我們?nèi)匀徽J為 localplus 被分成了四個區(qū)域,只不過對于外層函數(shù) some_func 而言,它的 free 區(qū)域長度為 0;對于 inner 函數(shù)而言,它的 cell 區(qū)域長度為 0。
當(dāng)然這些都是概念上的東西,大家理解就好。但不管在概念上 localplus 怎么劃分,它本質(zhì)上就是一個 C 數(shù)組,是一段連續(xù)的內(nèi)存,用于存儲局部變量、cell 變量、free 變量(這三種變量不一定同時存在),以及作為運行時棧。
最重要的是,這三種變量都是基于數(shù)組實現(xiàn)的靜態(tài)訪問,并且怎么訪問在編譯階段就已經(jīng)確定,因為訪問數(shù)組的索引會作為指令參數(shù)存儲在字節(jié)碼指令集中。
- 比如訪問變量 a,底層會訪問 localplus[0];
- 比如訪問變量 age,底層會訪問 localplus[3]->ob_ref;
這便是靜態(tài)訪問。
小結(jié)
本篇文章我們就介紹了閉包,比想象中的要更加簡單。因為閉包仍是一個函數(shù),只是將外層作用域的局部變量變成了 cell 變量,然后保存在內(nèi)部的 func_closure 字段中。
然后執(zhí)行內(nèi)層函數(shù)的時候,再將 func_closure 里的 PyCellObject * 拷貝到 localplus 的 free 區(qū)域,此時我們叫它 free 變量。但不管什么變量,虛擬機在編譯時便知道應(yīng)該如何訪問指定的內(nèi)存。