国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

閉包是怎么實現的?你知道嗎?

開發 前端
本篇文章我們就介紹了閉包,比想象中的要更加簡單。因為閉包仍是一個函數,只是將外層作用域的局部變量變成了 cell 變量,然后保存在內部的 func_closure 字段中。

楔子

在之前的文章中一直反復提到四個字:名字空間。一段代碼執行的結果不光取決于代碼中的符號,更多的是取決于代碼中符號的語義,而這個運行時的語義正是由名字空間決定的。

名字空間是由虛擬機在運行時動態維護的,但有時我們希望將名字空間靜態化。換句話說,我們希望有的代碼不受名字空間變化帶來的影響,始終保持一致的功能該怎么辦呢?隨便舉個例子:

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,于是可以通過使用嵌套函數來設置一個基準值。

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("古明地戀"))  # 歡迎: 古明地戀

盡管函數 login 里面沒有 user_name 和 password 這兩個局部變量,但是不妨礙我們使用它,因為外層函數 deco 里面有。

也就是說,函數 login 作為函數 deco 的返回值被返回的時候,有一個名字空間就已經和 login 緊緊地綁定在一起了。執行內層函數 login 的時候,對于自身 local 空間中不存在的變量,會從和自己綁定的 local 空間里面去找,這就是一種將名字空間靜態化的方法。這個名字空間和內層函數捆綁之后的結果我們稱之為閉包(closure)。

為了描述方便,上面說的是 local 空間,但我們知道,局部變量不是從那里查找的,而是從 localsplus 里面。只是我們可以按照 LEGB 的規則去理解,這一點心理清楚就行。

也就是說:閉包=外部作用域+內層函數。并且在介紹函數的時候提到,PyFunctionObject 是虛擬機專門為字節碼指令的傳輸而準備的大包袱,global 名字空間、默認參數都和字節碼指令捆綁在一起,同樣的,也包括閉包。

實現閉包的基石

閉包的創建通常是利用嵌套函數來完成的,我們說過局部變量是通過數組靜態存儲的,而閉包也是如此。這里再來回顧一下 PyCodeObject 里面的幾個關鍵字段:

  • co_localsplusnames:包含所有局部變量、cell 變量、free 變量的名稱
  • co_nlocalsplus:co_localsplusnames 的長度,或者說這些變量的個數之和
  • co_varnames:包含所有局部變量的名稱
  • co_nlocals:局部變量的個數
  • co_cellvars:包含所有 cell 變量的名稱
  • co_ncellvars:cell 變量的個數
  • co_freevars:包含所有 free 變量的名稱
  • co_nfreevars:free 變量的個數

因此不難得出它們之間的關系:

  • 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)  # ()

和閉包相關的兩個字段是 co_cellvars 和 co_freevars。co_cellvars 保存了外層作用域中被內層作用域引用的變量的名字,co_freevars 保存了內層作用域中引用的外層作用域的變量的名字。

所以對于外層函數來說,應該使用 co_cellvars,對于內層函數來說,應該使用 co_freevars。當然無論是外層函數還是內層函數都有 co_cellvars 和 co_freevars,這是肯定的,因為都是函數。

只不過外層函數需要使用 co_cellvars 獲取,因為它包含的是外層函數中被內層函數引用的變量的名稱;內層函數需要使用 co_freevars 獲取,它包含的是內層函數中引用的外層函數的變量的名稱。

如果使用外層函數 foo 獲取 co_freevars 的話,那么得到的結果顯然就是個空元組了,除非 foo 也作為某個函數的內層函數,并且內部引用了外層函數的變量。同理內層函數 bar 也是一樣的道理,它獲取 co_cellvars 得到的也是空元組,因為對于 bar 而言不存在內層函數。

我們再看個例子:

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')

對于函數 bar 而言,它是函數 inner 的外層函數,同時也是函數 foo 的內層函數。所以它在獲取 co_cellvars 和 co_freevars 屬性時,得到的元組都不為空。因為內層函數 inner 引用了函數 bar 里面的變量 gender,同時函數 bar 也作為內層函數引用了函數 foo 里的 name 和 age。

那么問題來了,閉包變量所需要的空間申請在哪個地方呢?沒錯,顯然是 localsplus。

在以前的版本中,這個字段叫 f_localsplus,現在叫 localsplus。

localplus 是一個柔性數組,它被分成了四份,分別用于:局部變量、cell 變量、free 變量、運行時棧。

所以閉包變量同樣是以靜態的方式實現的。

閉包的實現過程

介紹完實現閉包的基石之后,我們可以開始追蹤閉包的具體實現過程了,當然還是要先看一下閉包對應的字節碼。

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"))

字節碼指令如下,為了閱讀方便,我們省略了源代碼行號。

# ********** 模塊對應的字節碼 **********
  0 RESUME                   0
  # 加載函數 some_func 對應的 PyCodeObject,壓入運行時棧
  2 LOAD_CONST               0 (<code object some_func at ...>)
  # 從棧頂彈出 PyCodeObject,構造 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)
  # 調用
 12 CALL                     0
  # 彈出棧頂的返回值,并使用變量 func 保存
 20 STORE_NAME               1 (func)

 22 PUSH_NULL
  # 加載全局變量 func
 24 LOAD_NAME                1 (func)
  # 調用
 26 CALL                     0
  # 從棧頂彈出返回值,丟棄
 34 POP_TOP
  # 隱式地 return None
 36 RETURN_CONST             1 (None)

  # ********** 外層函數 some_func 對應的字節碼 **********
Disassembly of <code object some_func at ...>:
  # 創建 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 是通過數組完成綁定
  # 那么 STORE_DEREF 是怎么綁定的呢?稍后分析  
  8 STORE_DEREF              3 (name)
  # 加載常量 17
 10 LOAD_CONST               2 (17)
  # 使用變量 age 保存
 12 STORE_DEREF              2 (age)
  # name 和 age 被內層函數引用了,所以是 STORE_DEREF
  # 但 gender 沒有,所以它對應的是 STORE_FAST
 14 LOAD_CONST               3 ('female')
 16 STORE_FAST               0 (gender)
  # 加載 cell 變量,壓入運行時棧
 18 LOAD_CLOSURE             2 (age)
 20 LOAD_CLOSURE             3 (name)
  # 彈出 cell 變量,構建元組
 22 BUILD_TUPLE              2
  # 加載函數 inner 對應的 PyCodeObject
 24 LOAD_CONST               4 (<code object inner at ...>)
  # 構造函數
 26 MAKE_FUNCTION            8 (closure)
  # 將函數使用 inner 變量保存
 28 STORE_FAST               1 (inner)
  # return inner
 30 LOAD_FAST                1 (inner)
 32 RETURN_VALUE
 
 # ********** 內層函數 inner 對應的字節碼 **********
Disassembly of <code object inner at ...>:
  0 COPY_FREE_VARS           2

  2 RESUME                   0
  # 加載內置變量 print
  4 LOAD_GLOBAL              1 (NULL + print)
  # 顯然它和 LOAD_NAME、LOAD_FAST 的關系也是類似的
  # 也是負責加載變量,然后壓入運行時棧  
 14 LOAD_DEREF               1 (name)
 16 LOAD_DEREF               0 (age)
  # 調用 print 函數
 18 CALL                     2
  # 從棧頂彈出返回值,丟棄
 26 POP_TOP
 28 RETURN_CONST             0 (None)

字節碼的內容并不難,我們來分析一下,這里先分析外層函數 some_func 對應的字節碼。

圖片圖片

函數 some_func 里面有三個局部變量,但只有 name 和 age 被內層函數引用了,所以開頭有兩個 MAKE_CELL 指令。參數為符號在符號表中的索引,對應的符號分別為 age 和 name。我們來看一下這個指令是做什么的。

TARGET(MAKE_CELL) {
    #line 1394 "Python/bytecodes.c"
    // 符號在符號表中的索引,和對應的值在 localplus 中的索引是一致的
    // 所以這里會獲取變量對應的值,對于當前來說就是 age 和 name 的值
    // 但很明顯此時 name 和 age 還沒有完成賦值,所以結果為 NULL
    PyObject *initial = GETLOCAL(oparg);
    // 調用 PyCell_New 創建 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 申請內存
    op = (PyCellObject *)PyObject_GC_New(PyCellObject, &PyCell_Type);
    if (op == NULL)
        return NULL;
    // 增加 obj 指向對象的引用計數,并賦值給 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 指令的作用是創建 PyCellObject,對于當前來說,會創建兩個 PyCellObejct,它們的 ob_ref 字段分別為 age 和 name。只不過由于 name 和 age 還尚未完成賦值,所以此時為 NULL。

圖片圖片

接下來就是變量賦值,這個顯然沒什么難度,我們只需要看一下 STORE_DEREF 指令。并且也容易得出結論,如果局部變量被內層函數所引用,那么指令將不再是 LOAD_FAST 和 STORE_FAST,而是 LOAD_DEREF 和 STORE_DEREF。

TARGET(STORE_DEREF) {
    // 由于在 STORE_DEREF 之前調用了 LOAD_CONST
    // 所以這里會獲取上一步壓入的常量,對于當前來說就是 17 和 "satori"
    PyObject *v = stack_pointer[-1];
    #line 1463 "Python/bytecodes.c"
    // 這里的 oparg 和 MAKE_CELL 的 oparg 的含義是一樣的
    // 此時會拿到對應的 PyCellObject *
    PyObject *cell = GETLOCAL(oparg);
    // 獲取 PyCellObject 內部的 ob_ref,并減少引用計數
    PyObject *oldobj = PyCell_GET(cell);
    // 將 PyCellObject 內部的 ob_ref 設置成 v
    PyCell_SET(cell, v);
    Py_XDECREF(oldobj);
    #line 1993 "Python/generated_cases.c.h"
    // 棧收縮
    STACK_SHRINK(1);
    DISPATCH();
}

localplus 保存了局部變量的值,而符號在符號表中的索引,和對應的值在 localplus 中的索引是一致的。所以正常情況下,局部變量賦值就是 localsplus[oparg] = v。

但在執行 MAKE_CELL 指令之后,局部變量賦值就變成了 localsplus[oparg]->ob_ref = v,因為此時 localplus 保存的是 PyCellObject 的地址。

因此在兩個 STORE_DEREF 執行完之后,localplus 會變成下面這樣。

相信你明白 STORE_FAST 和 STORE_DEREF 之間的區別了,如果是 STORE_FAST,那么中間就沒有 PyCellObject 這一層,localsplus 保存的 PyObject * 指向的就是具體的對象。

然后是 gender = "female",它就很簡單了,由于符號 "gender" 在符號表中的索引為 0,那么直接讓 localplus[0] 指向字符串 "female" 即可。

到此變量 name、age、gender 均已賦值完畢,此時 localsplus 結構如下。

圖片圖片

localsplus[0]、localsplus[2]、localsplus[3] 分別對應變量 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,只不過此時它指向的對象還沒有創建,所以暫時為 NULL。

那么問題來了,變量 name 和 age 呢?毫無疑問,由于它們被內層函數引用了,所以它們變成了 cell 變量,并且位置是 co->co_nlocals + i。因為在 localsplus 中,cell 變量的位置是在局部變量之后的,這也完全符合我們之前說的 localsplus 的內存布局。

圖片圖片

并且我們看到無論是局部變量還是 cell 變量,都是通過數組索引訪問的,并且索引在編譯時就確定了,以指令參數的形式保存在字節碼指令集中。

接下來執行偏移量為 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 執行完畢后,接著執行 BUILD_TUPLE,將 cell 變量從棧中彈出,構建元組。然后繼續執行 24 LOAD_CONST,將內層函數 inner 對應的 PyCodeObject 壓入運行時棧。

接著執行 26 MAKE_FUNCTION,將棧中元素彈出,分別是 inner 對應的 PyCodeObject 和一個元組,元組里面包含了 inner 使用的外層函數的變量。當然這里的變量已經不再是普通的變量了,而是 cell 變量,它內部的 ob_ref 字段才是我們需要的。

等元素彈出之后,開始構建函數,我們看一下 MAKE_FUNCTION 指令,它的指令參數為 8。

TARGET(MAKE_FUNCTION) {
    PyObject *codeobj = stack_pointer[-1];
    PyObject *closure = (oparg & 0x08) ? stack_pointer[...] : NULL;
    // ...
    // 創建函數對象
    PyFunctionObject *func_obj = (PyFunctionObject *)
        PyFunction_New(codeobj, GLOBALS());

    Py_DECREF(codeobj);
    if (func_obj == NULL) {
        goto error;
    }
    // 由于指令參數為 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 再一次承擔了工具人的角色,創建內層函數 inner 時,會將包含 cell 變量的元組賦值給 func_closure 字段。此時便將內層函數需要使用的變量和內層函數綁定在了一起,而這個綁定的結果我們就稱之為閉包。

但是從結構上來看,閉包仍是一個函數,所謂綁定,其實只是修改了它的 func_closure 字段。當函數創建完畢后,localplus 的結構變化如下。

圖片圖片

函數即變量,對于函數 some_func 而言,內層函數 inner 也是一個局部變量,由于符號 inner 位于符號表中索引為 1 的位置。因此當函數創建完畢時,會修改 localplus[1],讓它保存函數的地址。不難發現,對于局部變量來說,如何訪問內存在編譯階段就確定了。

函數內部的 func_closure 字段指向一個元組,元組里面的每個元素會指向 PyCellObject。

調用閉包

閉包的創建過程我們已經了解了,我們用 Python 代碼再解釋一下。

def some_func():
    name = "satori"
    age = 17
    gender = "female"
    def inner():
        print(name, age)

    return inner

func = some_func()
# some_func 調用之后會返回內層函數 inner
# 只不過 inner 的 func_closure 字段保存了 cell 變量
# 而 cell 變量指向的 PyCellObject 對外層作用域的局部變量進行了凍結
# 所以我們也會稱呼 inner 函數為閉包,但要知道閉包仍然是個函數
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

調用 inner 函數時,外層函數 some_func 已經執行結束,但它的局部變量 name 和 age 仍可被內層函數 inner 訪問,背后的原因我們算是徹底明白了。

因為 name 和 age 被內層函數引用了,所以虛擬機將它們封裝成了 PyCellObject *,即 cell 變量,而 cell 變量指向的 cell 對象內部的 ob_ref 字段對應原來的變量。當創建內層函數時,將引用的 cell 變量組成元組,保存在內層函數的 func_closure 字段中。

所以當內層函數在訪問 name 和 age 時,訪問的其實是 PyCellObject 的 ob_ref 字段。至于變量 name 和 age 對應哪一個 PyCellObject,這些在編譯階段便確定了,我們看一下內層函數 inner 的字節碼指令。

圖片圖片

函數在執行時會創建棧幀,我們上面看到的 localsplus 是外層函數 some_func 對應的棧幀的 localsplus。而內層函數 inner 執行時,也會創建棧幀,然后在棧幀中執行字節碼指令。

首先第一個指令是 COPY_FREE_VARS,看一下它的邏輯。

// 將 func_closure 里面的 cell 變量拷貝到 free 區域
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 變量的個數之和
    // 顯然 offset 表示 free 變量對應的內存區域
    int offset = co->co_nlocalsplus - oparg;
    // 將 func_closure 里面的 PyCellObject * 拷貝到 free 區域
    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 的布局如下,注意:此時是內層函數對應的 localplus。

圖片圖片

在構建內層函數時,會將 cell 變量打包成一個元組,交給內層函數的 func_closure 字段。然后執行內層函數創建棧幀的時候,再將 func_closure 中的 cell 變量拷貝到 localsplus 的第三段內存中。當然對于內層函數而言,此時它應該叫做 free 變量。

而在調用內層函數 inner 的過程中,當引用外層作用域的符號時,一定是到 localsplus 里面的 free 區域(第三段內存)去獲取對應的 PyCellObject *,然后通過內部的 ob_ref 進而獲取符號對應的值。至于 name 和 age 分別對應哪一個 PyCellObject,這些都體現在字節碼指令參數當中了。

然后我們再來看看 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 是一個連續的數組,只是按照用途被劃分成了四個區域:保存局部變量的內存空間、保存 cell 變量的內存空間、保存 free 變量的內存空間、運行時棧。

但對于當前的內層函數 inner 來說,它是沒有局部變量和 cell 變量的,所以 localsplus 開始的位置便是 free 區域。

當然不管是局部變量、cell 變量,還是 free 變量,它們都按照順序保存在 localplus 中,并且在編譯階段便知道它們在 localsplus 中的位置。比如我們將內層函數 inner 的邏輯修改一下。

圖片圖片

在 inner 里面創建了三個局部變量,那么它的字節碼會變成什么樣子呢?這里我們直接看 print 函數執行時的字節碼即可。

圖片圖片

因為 inner 里面沒有函數了,所以它不存在 cell 變量,里面只有局部變量和 free 變量。

圖片圖片

所以雖然我們說 localplus 被分成了四份,但是 cell 區域和 free 區域很少會同時存在。對于外層函數 some_func 來說,它沒有 free 變量,所以 free 區域長度為 0。而對于內層函數 inner 來說,它沒有 cell 變量,所以 cell 區域長度為 0。

只有函數的里面存在內層函數,并且外面存在外層函數,那么它才有可能同時包含 cell 變量和 free 變量。

但為了方便描述,我們仍然認為 localplus 被分成了四個區域,只不過對于外層函數 some_func 而言,它的 free 區域長度為 0;對于 inner 函數而言,它的 cell 區域長度為 0。

當然這些都是概念上的東西,大家理解就好。但不管在概念上 localplus 怎么劃分,它本質上就是一個 C 數組,是一段連續的內存,用于存儲局部變量、cell 變量、free 變量(這三種變量不一定同時存在),以及作為運行時棧。

最重要的是,這三種變量都是基于數組實現的靜態訪問,并且怎么訪問在編譯階段就已經確定,因為訪問數組的索引會作為指令參數存儲在字節碼指令集中。

  • 比如訪問變量 a,底層會訪問 localplus[0];
  • 比如訪問變量 age,底層會訪問 localplus[3]->ob_ref;

這便是靜態訪問。

小結

本篇文章我們就介紹了閉包,比想象中的要更加簡單。因為閉包仍是一個函數,只是將外層作用域的局部變量變成了 cell 變量,然后保存在內部的 func_closure 字段中。

然后執行內層函數的時候,再將 func_closure 里的 PyCellObject * 拷貝到 localplus 的 free 區域,此時我們叫它 free 變量。但不管什么變量,虛擬機在編譯時便知道應該如何訪問指定的內存。

責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2023-01-09 08:00:41

JavaScript閉包

2024-12-04 08:40:19

2024-02-19 00:00:00

Docker輕量級容器

2023-12-20 08:23:53

NIO組件非阻塞

2024-12-11 08:19:34

2024-06-20 08:06:30

2025-01-16 16:41:00

ObjectConditionJDK

2024-10-05 00:00:00

HTTPS性能HTTP/2

2024-01-15 12:16:37

2022-11-28 00:04:17

2024-07-30 08:22:47

API前端網關

2024-11-08 09:48:38

異步編程I/O密集

2022-11-16 08:43:30

Node.js模塊

2024-10-28 08:38:40

會員批量應用

2024-10-24 08:47:12

2025-06-27 09:32:47

GoRedis單線程

2024-03-19 08:01:54

服務熔斷軟件設計模式微服務

2024-02-19 07:44:52

虛擬機Java平臺

2023-07-11 00:12:05

2024-06-27 10:51:28

生成式AI領域
點贊
收藏

51CTO技術棧公眾號

最近中文视频在线| 欧美家庭影院| 久久免费国产| 久久精品精品电影网| 天堂资源在线观看| 国产成人av一区二区三区在线| 国产免费亚洲高清| 成人开心激情| 欧美老肥妇做.爰bbww| 亚洲少妇久久久| 全国精品久久少妇| 91久久久在线| 日韩最新在线| 日韩在线视频一区| 三级资源在线| 欧美网站一区二区| 性史性dvd影片农村毛片| 国产xxx精品视频大全| 高清国产一区| 欧美理论在线播放| 欧美黄色免费网站| 成人亚洲视频| 亚洲美女www午夜| 蜜桃视频在线观看www社区| 亚洲香肠在线观看| 国产原创精品在线| 99精品黄色片免费大全| av不卡在线免费观看| 亚洲永久免费| 国产综合18久久久久久| 久久久久久美女精品| 日本欧美精品在线| 久久精品色综合| 美女视频黄免费的亚洲男人天堂| 国产传媒av在线| 精品粉嫩超白一线天av| 日韩另类在线| 欧美tickling网站挠脚心| 在线免费观看黄| 欧美日高清视频| 8888四色奇米在线观看| 欧美中文字幕不卡| porn亚洲| 欧美日韩另类一区| 尤物网址在线观看| 欧美二区三区的天堂| 大乳在线免费观看| 欧美日韩三级视频| 2024最新电影免费在线观看| 日韩欧美一级二级三级久久久| 欧美一级二级三级区| 欧美精品日韩一本| av免费网站在线观看| 精品少妇一区二区三区免费观看| 国产福利在线免费观看| 亚洲精品电影在线观看| 日韩性xxx| 久久福利视频导航| 神马日本精品| 99re热精品| 日韩国产欧美视频| 青青视频免费在线观看| www国产精品av| gay视频丨vk| 欧美日韩国产色| 精品孕妇一区二区三区| 亚洲精品久久久久国产| 成人免费91| 国产精品天天狠天天看| aⅴ色国产欧美| 91传媒免费视频| 中文子幕无线码一区tr| 香蕉97视频观看在线观看| 91精品国产综合久久久久久| 免费高潮视频95在线观看网站| 丝袜亚洲另类欧美重口| 欧美高清视频看片在线观看 | 另类春色校园亚洲| 成人黄色片在线| av一本在线| 精精国产xxxx视频在线野外| 日韩一二三区不卡| 欧美成人精品一区二区男人小说| 久久精品国产69国产精品亚洲| 亚洲综合av一区| 黄色免费网站在线观看| 亚洲精品720p| а√中文在线天堂精品| 国产免费成人av| 国产美女娇喘av呻吟久久| 逼特逼视频在线| 欧美亚洲国产怡红院影院| 日本成人伦理电影| 国产精品精品视频| 老牛嫩草一区二区三区日本 | 欧美黄色激情| 日韩在线视频中文字幕| 99精品视频在线| 久久久久亚洲av无码专区喷水| 国产精品久久久久永久免费观看| 在线观看免费黄色| 欧美高清在线播放| 国产亚洲在线| 午夜国产一区二区三区| 制服丝袜一区二区三区| 给我免费播放日韩视频| 日韩精彩视频| 亚洲精品欧美激情| 欧美freesex| 91久久精品美女高潮| 成人的网站免费观看| 性色av一区| 免费不卡在线观看av| 国产欧美日韩综合一区在线播放 | 欧美高清不卡| 国产欧美在线一区| 欧美高清精品3d| 五月国产精品| www.69av| 欧美高清dvd| 成人羞羞动漫| 男人舔女人下面高潮视频| 日韩三级av在线播放| 黑丝美女一区二区| 日日橹狠狠爱欧美超碰| 日韩视频123| 欧美高清不卡| 偷偷要 色偷偷| 欧美精品一区二区三区国产精品| 日韩国产精品大片| 国产女人在线观看| 国产精品7m视频| 久久午夜国产精品| 免费观看一级欧美片| 欧美一区二区三区在线播放| 亚洲6080在线| 最新亚洲精品| www.激情小说.com| 色妞欧美日韩在线| 青青草精品视频| www免费视频观看在线| 91欧美激情另类亚洲| 国产精品嫩草久久久久| 欧美视频第一| av久久久久久| 国产亚洲aⅴaaaaaa毛片| 日韩黄色小视频| av在线网址观看| 欧美裸体网站| 日韩一区二区三区av| 欧美日韩国产探花| 欧美孕妇孕交| 99久久综合狠狠综合久久止 | 国产999精品| 国产精品三级视频| 久久伊人久久| 国产成人精品视频ⅴa片软件竹菊| 在线观看国产精品日韩av| 国产美女视频一区| 外国电影一区二区| 日本国产中文字幕| 亚洲欧美日韩天堂一区二区| 国产一区二区福利视频| 91在线三级| 精品久久久无码人妻字幂| 原创国产精品91| 91网址在线看| 999在线精品| 美女做a视频| 91日韩在线视频| 欧美浪妇xxxx高跟鞋交| 六月婷婷一区| 国产精品蜜芽在线观看| 国产 欧美 日本| 色综合久久精品亚洲国产 | 免费黄色在线看| 蜜桃传媒视频麻豆第一区免费观看| 欧美精品乱码久久久久久按摩| 一本一道久久综合狠狠老精东影业| 麻豆网站在线观看| 一区二区不卡在线| 日韩在线小视频| 国产精品的网站| 91精品国产调教在线观看| 九色视频在线播放| 婷婷四房综合激情五月| 亚洲片av在线| 91亚洲大成网污www| 亚洲最大在线| 色综合久久影院| 激情六月天婷婷| 97人人模人人爽人人喊中文字| 午夜精品久久久久久久| 国产精品久久国产愉拍| 456成人影院在线观看| 69中国xxxxxxxxx69| 国产福利一区二区三区在线观看| 亚洲精品久久久久中文字幕欢迎你 | 91麻豆精品| 免费在线黄网|