成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

局部變量是怎么實現靜態查找的,它和 local 名字空間又有什么聯系呢?

開發 前端
我們說過,當調用 locals 的時候,會對名字空間進行更新,然后返回更新之后的名字空間。由于函數內部存在 y = ... 這樣的賦值語句,所以符號表中就存在 "y" 這個符號,于是會進行更新。但更新的時候,發現 y 還沒有被賦值,于是又將字典中的鍵值對 "y": 2 給刪掉了。


楔子


前面我們剖析了字節碼的執行流程,本來應該接著介紹一些常見指令的,但因為有幾個指令涉及到了局部變量,所以我們單獨拿出來說。與此同時,我們還要再度考察一下 local 名字空間,它的背后還隱藏了很多內容。

我們知道函數的參數和函數內部定義的變量都屬于局部變量,均是通過靜態方式訪問的。

x = 123

def foo1():
    global x
    a = 1
    b = 2

# co_nlocals 會返回局部變量的個數
# a 和 b 是局部變量,x 是全局變量,因此是 2
print(foo1.__code__.co_nlocals)  # 2


def foo2(a, b):
    pass

print(foo2.__code__.co_nlocals)  # 2


def foo3(a, b):
    a = 1
    b = 2
    c = 3

print(foo3.__code__.co_nlocals)  # 3

無論是參數還是內部新創建的變量,本質上都是局部變量。

按照之前的理解,當訪問一個全局變量時,會去訪問 global 名字空間(也叫全局名字空間)。

圖片

那么問題來了,當操作函數的局部變量時,是不是也等價于操作其內部的 local 名字空間(局部名字空間)呢?我們往下看。


圖片


如何訪問(創建)一個局部變量


之前我們說過 Python 變量的訪問是有規則的,會按照本地、閉包、全局、內置的順序去查找,也就是 LEGB 規則,所以在查找變量時,local 名字空間應該是第一選擇。

但不幸的是,虛擬機在為調用的函數創建棧幀對象時,這個至關重要的 local 名字空間并沒有被創建。因為棧幀的 f_locals 字段和 f_globals 字段分別指向了局部名字空間和全局名字空間,而創建棧幀時 f_locals 被初始化成了 NULL,所以并沒有創建局部名字空間。

我們通過源碼來進行驗證,不過要先補充一個知識點,就是當調用一個 Python 函數時,底層會調用哪些 C 函數呢?

圖片

我們看一下源碼:

// Objects/call.c
/*
 * Python 函數也是一個對象,當調用 Python 函數時
 * 底層會將 Python 函數對象作為參數,調用 _PyFunction_Vectorcall
 * 關于函數,我們后續會剖析
 */
PyObject *
_PyFunction_Vectorcall(PyObject *func, PyObject* const* stack,
                       size_t nargsf, PyObject *kwnames)
{
    // ...
    
    // 參數 func 指向函數對象,它內部的 func_code 指向 PyCodeObject 對象
    // 如果 co_flags & CO_OPTIMIZED 為真,表示 PyCodeObject 是被優化過的
    // 那么對應的函數在調用時,會靜態查找本地局部變量
    if (((PyCodeObject *)f->func_code)->co_flags & CO_OPTIMIZED) {
        // 在這種情況下,會給 _PyEval_Vector 的第三個參數傳遞 NULL
        return _PyEval_Vector(tstate, f, NULL, stack, nargs, kwnames);
    }
    // ...
}


// Python/ceval.c
/*
 * 創建棧幀,調用 _PyEval_EvalFrame,最終執行幀評估函數
 * 注意該函數的第三個參數,顯然它表示局部名字空間
 * 而 _PyFunction_Vectorcall 在調用時傳遞的是 NULL
 */
PyObject *
_PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func,
               PyObject *locals,
               PyObject* const* args, size_t argcount,
               PyObject *kwnames)
{
    // ...
    
    // 執行幀評估函數之前,要先創建棧幀
    // 這個過程由 _PyEvalFramePushAndInit 負責
    _PyInterpreterFrame *frame = _PyEvalFramePushAndInit(
        tstate, func, locals, args, argcount, kwnames);
    // ...
    return _PyEval_EvalFrame(tstate, frame, 0);
}

/*
 * 在當前棧幀之上創建新的棧幀,并推入虛擬機為其準備的 C Stack 中
 */
static _PyInterpreterFrame *
_PyEvalFramePushAndInit(PyThreadState *tstate, PyFunctionObject *func,
                        PyObject *locals, PyObject* const* args,
                        size_t argcount, PyObject *kwnames)
{
    // ...

    // 棧幀創建之后,調用 _PyFrame_Initialize,進行初始化
    _PyFrame_Initialize(frame, func, locals, code, 0);
    // ...
}

// Include/internal/pycore_frame.h
/*
 * 對 frame 進行初始化
 */
staticinlinevoid
_PyFrame_Initialize(
    _PyInterpreterFrame *frame, PyFunctionObject *func,
    PyObject *locals, PyCodeObject *code, int null_locals_from)
{
    frame->f_funcobj = (PyObject *)func;
    frame->f_code = (PyCodeObject *)Py_NewRef(code);
    frame->f_builtins = func->func_builtins;
    frame->f_globals = func->func_globals;
    // 將 f_locals 字段初始化為參數 locals
    // 而參數 locals 是從 _PyFunction_Vectorcall 一層層傳過來的
    // 由于 _PyFunction_Vectorcall 傳的是 NULL
    // 所以棧幀的 f_locals 字段最終會被初始化為 NULL
    frame->f_locals = locals;
    // ...
}

所以我們驗證了在調用函數時,棧幀的局部名字空間確實被初始化為 NULL,當然也明白了 C 函數的調用鏈路。

我們用 Python 代碼演示一下:

import inspect

# 模塊的棧幀
frame = inspect.currentframe()
# 對于模塊而言,局部名字空間和全局名字空間是同一個字典
print(frame.f_locals is frame.f_globals)  # True
# 當然啦,局部名字空間和全局名字空間也可以通過內置函數獲取
print(
    frame.f_locals is locals() is frame.f_globals is globals()
)  # True


# 但對于函數而言就不一樣了
def foo():
    name = "古明地覺"
    return inspect.currentframe()

frame = foo()
# global 名字空間全局唯一
# 無論是獲取棧幀的 f_globals,還是調用 globals()
# 得到的都是同一份字典
print(frame.f_globals is globals())  # True
# 但每個函數都有自己獨立的局部名字空間
print(frame.f_locals)  # {'name': '古明地覺'}

# 咦,不是說局部名字空間被初始化為 NULL 嗎?
# 那么在 Python 里面獲取的話,結果應該是個 None 才對啊
# 關于這一點,我們稍后會解釋

總之對于函數而言,在創建棧幀時,它的 f_locals 被初始化為 NULL。那么問題來了,局部變量到底存儲在什么地方呢?當然,由于變量只是一個名字(符號),而局部變量的名字都存儲在符號表中,所以更嚴謹的說法是,局部變量的值存儲在什么地方?

在介紹虛擬機執行字節碼的時候我們說過,當函數被調用時,虛擬機會為其創建一個棧幀。棧幀是虛擬機的執行環境,包含了執行時所依賴的上下文,而棧幀內部有一個字段叫 f_localsplus,它是一個數組。

圖片圖片

這個數組雖然是一段連續內存,但在邏輯上被分成了 4 份,其中局部變量便存儲在 f_localsplus 的第一份空間中。現在我們明白了,局部變量是靜態存儲在數組中的。

我們舉個例子。

def foo(a, b):
    c = a + b
    print(c)

它的字節碼如下:

圖片

注意里面的 LOAD_FAST 和 STORE_FAST,這兩個指令對應的邏輯如下。

TARGET(LOAD_FAST) {
    PyObject *value;
    #line 192 "Python/bytecodes.c"
    // 通過宏 GETLOCAL 獲取局部變量的值
    value = GETLOCAL(oparg);
    assert(value != NULL);
    Py_INCREF(value);
    #line 90 "Python/generated_cases.c.h"
    // 將值壓入運行時棧,等價于 PUSH(value)
    STACK_GROW(1);
    stack_pointer[-1] = value;
    DISPATCH();
}

TARGET(STORE_FAST) {
    // 獲取棧頂元素
    PyObject *value = stack_pointer[-1];
    #line 209 "Python/bytecodes.c"
    // 通過宏 SETLOCAL 創建局部變量
    SETLOCAL(oparg, value);
    #line 124 "Python/generated_cases.c.h"
    // 將 stack_pointer 向棧底移動一個位置,即彈出棧頂元素
    // 如果和第一行組合起來的話,等價于 TOP()
    STACK_SHRINK(1);
    DISPATCH();
}

所以 LOAD_FAST 和 STORE_FAST 分別負責加載和創建局部變量,而核心就是里面的兩個宏:GETLOCAL、SETLOCAL。

// Python/ceval_macros.h

#define GETLOCAL(i)     (frame->localsplus[i])

#define SETLOCAL(i, value)      do { PyObject *tmp = GETLOCAL(i); \
                                     GETLOCAL(i) = value; \
                                     Py_XDECREF(tmp); } while (0)
/* 這里額外再補充一個關于 C 語言的知識點
 * 我們看到宏 SETLOCAL 展開之后的結果是 do {...} while (0) 
 * do while 循環會先執行 do 里面的循環體,然后再判斷條件是否滿足
 * 因此從效果上來說,執行 do {...} while (0) 和直接執行 ... 是等價的
 * 那么問題來了,既然效果等價,為啥還要再套一層 do while 呢
 * 其實原因很簡單,如果宏在展開之后會生成多條語句,那么這些語句要成為一個整體
 * 另外由于 C 程序的語句要以分號結尾,所以在調用宏時,我們也會習慣性地在結尾加上分號
 * 因此我們希望有這樣一種結構,能同時滿足以下要求:
 *   1)可以將多條語句包裹起來,作為一個整體;
 *   2)程序的語義不能發生改變;
 *   3)在語法上,要以分號結尾;
 * 顯然 do while 完美滿足以上三個要求,只需將 while 里的條件設置為 0 即可
 * 并且當編譯器看到 while (0) 時,也會進行優化,去掉不必要的循環控制結構
 * 因此以后看到 do {...} while (0) 時,不要覺得奇怪,這是宏的一個常用技巧
 */

我們看到操作局部變量,就是在基于索引操作數組 f_localsplus,顯然這個過程比操作字典要快。盡管字典是經過高度優化的,但顯然再怎么優化,也不可能快過數組的靜態操作。

所以此時我們對局部變量的藏身之處已經了然于心,它們就存放在棧幀的 f_localsplus 字段中,而之所以沒有使用 local 名字空間的原因也很簡單。因為函數內部的局部變量在編譯時就已經確定了,個數是不會變的,因此編譯時也能確定局部變量占用的內存大小,以及訪問局部變量的字節碼指令應該如何訪問內存。

def foo(a, b):
    c = a + b
    print(c)

print(
    foo.__code__.co_varnames
)  # ('a', 'b', 'c')

比如變量 c 位于符號表中索引為 2 的位置,這在編譯時就已確定。

  • 當創建變量 c 時,只需修改數組 f_localsplus 中索引為 2 的元素即可。
  • 當訪問變量 c 時,只需獲取數組 f_localsplus 中索引為 2 的元素即可。

這個過程是基于數組索引實現的靜態查找,所以操作局部變量和操作全局變量有著異曲同工之妙。操作全局變量本質上是基于 key 操作字典的 value,其中 key 是變量的名稱,value 是變量的值;而操作局部變量本質上是基于索引操作數組 f_localsplus 的元素,這個索引就是變量名在符號表中的索引,對應的數組元素就是變量的值。

所以我們說 Python 的變量其實就是個名字,或者說符號,到這里是不是更加深刻地感受到了呢?

但對于局部變量來說,如果想實現靜態查找,顯然要滿足一個前提:變量名在符號表中的索引和與之綁定的值在 f_localsplus 中的索引必須是一致的。毫無疑問,兩者肯定是一致的,并且索引是多少在編譯階段便已經確定,會作為指令參數保存在字節碼指令序列中。

好,到此可以得出結論,雖然虛擬機為函數實現了 local 名字空間(初始為 NULL),但在操作局部變量時卻沒有使用它,原因就是為了更高的效率。當然還有所謂的 LEGB,都說變量查找會遵循這個規則,但我們心里清楚,局部變量其實是靜態訪問的,不過完全可以按照 LEGB 的方式來理解。


圖片


解密 local 名字空間


先來看一下全局名字空間:

x = 1

def foo():
    globals()["x"] = 2
    
foo()
print(x)  # 2

global 空間全局唯一,在 Python 層面上就是一個字典,在任何地方操作該字典,都相當于操作全局變量,即使是在函數內部。

因此在執行完 foo() 之后,全局變量 x 就被修改了。但 local 名字空間也是如此嗎?我們嘗試一下。

def foo():
    x = 1
    locals()["x"] = 2
    print(x)


foo()  # 1

我們按照相同的套路,卻并沒有成功,這是為什么?原因就是上面解釋的那樣,函數內部有哪些局部變量在編譯時就已經確定了,查詢的時候是從數組 f_localsplus 中靜態查找的,而不是從 local 名字空間中查找。

然后我們打印一下 local 名字空間,看看里面都有哪些內容。

def foo():
    name = "satori"
    print(locals())
    age = 17
    print(locals())
    gender = "female"
    print(locals())

foo()
"""
{'name': 'satori'}
{'name': 'satori', 'age': 17}
{'name': 'satori', 'age': 17, 'gender': 'female'}
"""

我們看到打印 locals() 居然也會顯示內部的局部變量,相信聰明如你已經猜到 locals() 是怎么回事了。因為局部變量不是從局部名字空間里面查找的,所以它初始為空,但當我們執行 locals() 的時候,會動態構建一個字典出來。

符號表里面存儲了局部變量的符號(或者說名字),f_localsplus 里面存儲了局部變量的值,當執行 locals() 的時候,會基于符號表和 f_localsplus 創建一個字典出來。

def foo():
    name = "satori"
    age = 17
    gender = "female"
    print(locals())

# 符號表:保存了函數中創建的局部變量的名字
print(foo.__code__.co_varnames)
"""
('name', 'age', 'gender')
"""
# 調用函數時會創建棧幀,局部變量的值都保存在 f_localsplus 里面
# 并且符號表中變量名的順序和 f_localsplus 中變量值的順序是一致的
f_localsplus = ["satori", 17, "female"]
# 這里就用一個列表來模擬了

我們來看一下變量的創建。

  • 由于符號 name 位于符號表中索引為 0 的位置,那么執行 name = "satori" 時,就會將 "satori" 放在 f_localsplus 中索引為 0 的位置。
  • 由于符號 age 位于符號表中索引為 1 的位置,那么執行 age = 17 時,就會將 17 放在 f_localsplus 中索引為 1 的位置。
  • 由于符號 gender 位于符號表中索引為 2 的位置,那么執行 gender = "female" 時,就會將 "female" 放在 f_localsplus 中索引為 2 的位置。

后續在訪問變量的時候,比如訪問變量 age,由于它位于符號表中索引為 1 的位置,那么就會通過 f_localsplus[1] 獲取它的值,這些符號對應的索引都是在編譯階段確定的。所以在運行時才能實現靜態查找,指令 LOAD_FAST 和 STORE_FAST 都是基于索引來靜態操作底層數組。

我們用一張圖來描述這個過程:

圖片

符號表負責存儲局部變量的名字,f_localsplus 負責存儲局部變量的值(里面的元素初始為 NULL),而在給局部變量賦值的時候,本質上就是將值寫在了 f_localsplus 中。并且變量名在符號表中的索引,和變量值在 f_localsplus 中的索引是一致的,因此操作局部變量本質上就是在操作 f_localsplus 數組。

至于 locals() 或者說局部名字空間,它是基于符號表和 f_localsplus 動態創建的。為了方便我們獲取已存在的局部變量,執行 locals() 會臨時創建一個字典。

所以我們通過 locals() 獲取局部名字空間之后,訪問里面的局部變量是可以的,只不過此時將靜態訪問變成了動態訪問。

def foo():
    name = "satori"
    # 會從 f_localsplus 中靜態查找
    print(name)
    # 先基于已有的變量和值創建一個字典
    # 然后通過字典實現變量的動態查找
    print(locals()["name"])

foo()
"""
satori
satori
"""

兩種方式都是可以的,但基于 locals() 來訪問,在效率上明顯會低一些。

另外基于 locals() 訪問一個變量是可以的,但無法創建一個變量。

def foo():
    name = "satori"
    locals()["age"] = 17
    try:
        print(age)
    except NameError as e:
        print(e)

foo()
"""
name 'age' is not defined
"""

局部變量是靜態存儲在數組里的,locals() 只是做了一個拷貝而已。往局部名字空間里面添加一個鍵值對,不等于創建一個局部變量,因為局部變量不是從它這里查找的,因此代碼中打印 age 報錯了。但如果外部還有一個全局變量 age 的話,那么會打印全局變量 age。

然后再補充一點,我們說全局名字空間在任何地方都是唯一的,而對于函數而言,它的局部名字空間在整個函數內部也是唯一的。不管調用 locals 多少次,拿到的都是同一個字典。

def foo():
    name = "satori"
    # 執行 locals() 的時候,內部只有一個鍵值對
    d = locals()
    print(d)  # {'name': 'satori'}
    # 再次獲取,此時有兩個鍵值對
    print(locals())  # {'name': 'satori', 'd': {...}}
    
    # 但兩者的 id 相同,因為一個函數只有一個局部名字空間
    # 不管調用多少次 locals(),拿到的都是同一個字典
    print(id(d) == id(locals()))  # True

foo()

所以 locals() 和 globals() 指向的名字空間都是唯一的,只不過 locals() 是在某個函數內部唯一,而 globals() 在所有地方都唯一。

因此局部名字空間初始為 NULL,但在第一次執行 locals() 時,會以符號表中的符號作為 key,f_localsplus 中的值作為 value,創建一個字典作為函數的局部名字空間。而后續再執行 locals() 的時候,由于名字空間已存在,就不會再次創建了,直接基于當前的局部變量對字典進行更新即可。

def foo():
    # 創建一個字典,由于當前還沒有定義局部變量,因此是空字典
    print(locals())
    """
    {}
    """
    # 往局部名字空間添加一個鍵值對
    locals()["a"] = "b"
    print(locals())
    """
    {'a': 'b'}
    """
    # 定義一個局部變量
    name = "satori"
    # 由于局部名字空間已存在,因此不會再次創建
    # 直接將局部變量的名字作為 key、值作為 value,拷貝到字典中
    print(locals())
    """
    {'a': 'b', 'name': 'satori'}
    """

foo()

注意:雖然局部名字空間里面存在 "a" 這個 key,但 a 這個局部變量是不存在的。


圖片


local 名字空間的創建過程


目前我們已經知道 local 名字空間是怎么創建的了,也熟悉了它的特性,下面通過源碼來看一下它的構建過程。

// Python/bltinmodule.c
static PyObject *
builtin_locals_impl(PyObject *module)
{
    // Python 內置函數的源碼實現位于 bltinmodule.c 中
    // 這里又調用了 _PyEval_GetFrameLocals
    return _PyEval_GetFrameLocals();
}

// Python/ceval.c
PyObject *
_PyEval_GetFrameLocals(void)
{
    PyThreadState *tstate = _PyThreadState_GET();
     _PyInterpreterFrame *current_frame = _PyThreadState_GetFrame(tstate);
    if (current_frame == NULL) {
        _PyErr_SetString(tstate, PyExc_SystemError, "frame does not exist");
        return NULL;
    }
    // 調用了 _PyFrame_GetLocals
    return _PyFrame_GetLocals(current_frame, 1);
}

所以核心邏輯位于 _PyFrame_GetLocals 函數中,來看一下它的邏輯。

// Object/frameobject.c
PyObject *
_PyFrame_GetLocals(_PyInterpreterFrame *frame, int include_hidden)
{
    // 獲取局部名字空間
    PyObject *locals = frame->f_locals;
    // 如果為 NULL,那么創建一個新字典,作為名字空間
    // 所以局部名字空間只會創建一次,后續不會再創建
    if (locals == NULL) {
        locals = frame->f_locals = PyDict_New();
        if (locals == NULL) {
            return NULL;
        }
    }
    PyObject *hidden = NULL;

    // 在 Include/internal/pycore_code.h 里面有 4 個宏
    /* #define CO_FAST_HIDDEN  0x10
     * #define CO_FAST_LOCAL   0x20
     * #define CO_FAST_CELL    0x40
     * #define CO_FAST_FREE    0x80
     */
    // 它們分別對應隱藏變量、局部變量、cell 變量、free 變量
    // 所謂隱藏變量,指的就是解析式里的臨時變量,比如列表解析式
    // 解析式具有獨立的作用域,里面的臨時變量不會污染外部的作用域
    // 所以一般我們也不會關注這些隱藏變量,locals() 也不會返回它
    // 但如果你真的關注,那么可以將 include_hidden 指定為真
    // 那么調用 locals() 時,這些隱藏變量也會一塊兒返回
    if (include_hidden) {
        // 單獨創建一個字典,負責保存隱藏變量
        hidden = PyDict_New();
        if (hidden == NULL) {
            return NULL;
        }
    }
    // 初始化 free 變量,這個和閉包有關
    // 關于閉包,等剖析完函數之后會說,這里暫時先不關注
    frame_init_get_vars(frame);

    PyCodeObject *co = frame->f_code;
    // co_nlocalsplus 等于局部變量、cell 變量、free 變量的個數之和
    // 這些變量都要拷貝到 local 名字空間中
    for (int i = 0; i < co->co_nlocalsplus; i++) {
        PyObject *value;
        // 獲取 f_localsplus[i],在函數內部會對 value 進行修改
        if (!frame_get_var(frame, co, i, &value)) {
            continue;
        }
        // f_localsplus[i] 對應局部名字空間的 value
        // 那么 co_localsplusnames[i] 顯然對應局部名字空間的 key
        // 估計有人已經忘記 co_localsplusnames 字段的含義了,我們再解釋一下
        /* 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
         */
        // 所以 co_localsplusnames 也是符號表,并且是 co_varnames 的超集
        PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
        // 到此局部名字空間的 key 和 value 便有了,但還要做一個判斷
        _PyLocals_Kind kind = _PyLocals_GetKind(co->co_localspluskinds, i);
        // 如果變量的類型是隱藏變量,那么添加到 hidden 中
        // 所以 co_localsplusnames 其實還包含了隱藏變量的名稱
        // 但我們基本不會遇到這種情況,因此關于隱藏變量直接忽略掉即可
        if (kind & CO_FAST_HIDDEN) {
            if (include_hidden && value != NULL) {
                if (PyObject_SetItem(hidden, name, value) != 0) {
                    goto error;
                }
            }
            continue;
        }
        // 如果不是隱藏變量,那么拷貝到 locals 中,但這里有一個判斷很重要
        // 當 value 為 NULL 時,如果 key 已存在,那么會將它刪掉
        // 關于這里的玄機,稍后會解釋
        if (value == NULL) {
            if (PyObject_DelItem(locals, name) != 0) {
                if (PyErr_ExceptionMatches(PyExc_KeyError)) {
                    PyErr_Clear();
                }
                else {
                    goto error;
                }
            }
        }
        // 到這里說明 value 指向了一塊合法的內存
        // 也就是變量名和變量值已經完成了綁定,那么將它們添加到 locals 中
        else {
            if (PyObject_SetItem(locals, name, value) != 0) {
                goto error;
            }
        }
        // 繼續遍歷下一個符號
    }
    // 隱藏變量保存在 hidden 中,它不會污染 f_locals
    if (include_hidden && PyDict_Size(hidden)) {
        // 創建一個新字典
        PyObject *innerlocals = PyDict_New();
        if (innerlocals == NULL) {
            goto error;
        }
        // 合并 locals
        if (PyDict_Merge(innerlocals, locals, 1) != 0) {
            Py_DECREF(innerlocals);
            goto error;
        }
        // 合并 hidden
        if (PyDict_Merge(innerlocals, hidden, 1) != 0) {
            Py_DECREF(innerlocals);
            goto error;
        }
        // 重新賦值給 locals,所以返回的結果會包含 hidden 里的鍵值對
        // 但 f_locals 里面是沒有隱藏變量的
        locals = innerlocals;
    }
    else {
        Py_INCREF(locals);
    }
    Py_CLEAR(hidden);
    // 返回 locals
    return locals;

  error:
    Py_XDECREF(hidden);
    return NULL;
}

所以邏輯非常簡單,如果不考慮隱藏變量(也不需要考慮),那么整個過程就是我們剛才說的:遍歷符號表和 f_localsplus,將變量名和變量值組成鍵值對拷貝到字典中。

但里面有一處細節非常關鍵。

圖片

當變量值為 NULL 時,說明在獲取名字空間時,該變量還沒有被賦值。要是此時變量已經在局部名字空間中,那么會將它從名字空間中刪掉。這一處非常關鍵,在介紹 exec 的時候你就會明白。


圖片


local 名字空間與 exec 函數


我們再來搭配 exec 關鍵字,結果會更加明顯。首先 exec 函數可以將一段字符串當成代碼來執行,并將執行結果體現在當前的名字空間中。

def foo():
    print(locals())  # {}
    exec("x = 1")
    print(locals())  # {'x': 1}
    try:
        print(x)
    except NameError as e:
        print(e)  # name 'x' is not defined
        
foo()

盡管 locals() 變了,但是依舊訪問不到 x,因為虛擬機并不知道 exec("x = 1") 是創建一個局部變量,它只知道這是一個函數調用。

事實上 exec 會作為一個獨立的編譯單元來執行,并且有自己的作用域。

所以 exec("x = 1") 執行完之后,效果就是改變了局部名字空間,里面多了一個 "x": 1 鍵值對。但關鍵的是,局部變量 x 不是從局部名字空間中查找的,exec 終究還是錯付了人。

由于函數 foo 對應的 PyCodeObject 對象的符號表中并沒有 x 這個符號,所以報錯了。

補充:exec 默認影響的是 local 名字空間,如果在執行時發現 local 名字空間為 NULL,那么會自動創建一個。所以調用 exec 也可以創建名字空間(當它為 NULL 時)。

exec("x = 1")
print(x)  # 1

如果放在模塊里面是可以的,因為模塊的 local 名字空間和 global 名字空間指向同一個字典,所以 global 名字空間會多一個 key 為 "x" 的鍵值對。而全局變量是從 global 名字空間中查找的,所以這里沒有問題。

def foo():
    # 此時 exec 影響的是全局名字空間
    exec("x = 123", globals())
    # 這里不會報錯, 但此時的 x 不是局部變量, 而是全局變量
    print(x)

foo()
print(x)
"""
123
123
"""

可以給 exec 指定要影響的名字空間,代碼中 exec 影響的是全局名字空間,打印的 x 也是全局變量。

以上幾個例子都比較簡單,接下來我們開始上強度了。

def foo():
    exec("x = 1")
    print(locals()["x"])

foo()
"""
1
"""

def bar():
    exec("x = 1")
    print(locals()["x"])
    x = 123

bar()
"""
Traceback (most recent call last):
  File .....
    bar()
  File .....
    print(locals()["x"])
KeyError: 'x'
"""

這是什么情況?函數 bar 只是多了一行賦值語句,為啥就報錯了呢?其實背后的原因我們之前分析過。

1)函數的局部變量在編譯的時候已經確定,并存儲在對應的 PyCodeObject 對象的符號表中,這是由語法規則所決定的;

2)函數內的局部變量在其整個作用域范圍內都是可見的;

對于 foo 函數來說,exec 執行完之后相當于往 local 名字空間中添加一個鍵值對,這沒有問題。對于 bar 函數而言也是如此,在執行完 exec("x = 1") 之后,local 名字空間也會存在 "x": 1 這個鍵值對,但問題是下面執行 locals() 的時候又把字典更新了。

因為局部變量可以在函數的任意位置創建,或者修改,所以每一次執行 locals() 的時候,都會遍歷符號表和 f_localsplus,然后組成鍵值對拷貝到名字空間中。

在 bar 函數里面有一行 x  = 123,所以知道函數里面存在局部變量 x,符號表里面也會有 "x" 這個符號,這是在編譯時就確定的。但我們是在 x = 123 之前調用的 locals,所以此時符號 x 在 f_localsplus 中對應的值還是一個 NULL,沒有指向一個合法的 PyObject。換句話說就是,知道里面存在局部變量 x,但此時尚未賦值。

然后在更新名字空間的時候,如果發現值是個 NULL,那么就把名字空間中該變量對應的鍵值對給刪掉。

圖片圖片

所以 bar 函數執行 locals()["x"] 的時候,會先獲取名字空間,原本里面是有 "x": 1 這個鍵值對的。但因為賦值語句 x = 123 的存在,導致符號表里面存在 "x" 這個符號,可執行 locals() 的時候又尚未完成賦值,因此值為 NULL,于是又把這個鍵值對給刪掉了。所以執行 locals()["x"] 的時候,出現了 KeyError。

因為局部名字空間體現的是局部變量的值,而調用 locals 的時候,局部變量 x 還沒有被創建。所以 locals() 里面不應該存在 key 為 "x" 的鍵值對,于是會將它刪除。

我們將名字空間打印一下:

def foo():
    # 創建局部名字空間,并寫入鍵值對 "x": 1
    # 此時名字空間為 {"x": 1}
    exec("x = 1")
    # 獲取名字空間,會進行更新
    # 但當前不存在局部變量,所以名字空間仍是 {"x": 1}
    print(locals())

def bar():
    # 創建局部名字空間,并寫入鍵值對 "x": 1
    # 此時名字空間為 {"x": 1}
    exec("x = 1")
    # 獲取名字空間,會進行更新
    # 由于里面存在局部變量 x,但尚未賦值
    # 于是將字典中 key 為 "x" 的鍵值對給刪掉
    # 所以名字空間變成了 {}
    print(locals())
    x = 123


foo()  # {'x': 1}
bar()  # {}

上面代碼中,局部變量的創建發生在 exec 之后,如果發生在 exec 之前也是類似的結果。

def foo():
    exec("x = 2")
    print(locals())

foo()  # {'x': 2}


def bar():
    x = 1
    exec("x = 2")
    print(locals())

bar()  # {'x': 1}

在 exec("x = 2") 執行之后,名字空間也變成了 {"x": 2}。但每次調用 locals,都會對字典進行更新,所以在 bar 函數里面獲取名字空間的時候,又把 "x" 對應的 value 給更新回來了。

當然這是在變量沖突的情況下,會保存真實存在的局部變量的值。如果不沖突,比如 bar 函數里面是 exec("y = 2"),那么 locals() 里面就會存在兩個鍵值對,但只有 x 才是真正的局部變量,而 y 則不是。

將 exec("x = 2") 換成 locals()["x"] = 2 也是一樣的效果,它們都是往局部名字空間中添加一個鍵值對,但不會創建一個局部變量。

薛定諤的貓

當 Python 中混進一只薛定諤的貓……,這是《Python 貓》在 19 年更新的一篇文章,里面探討的內容和我們本文的主題是重疊的。貓哥在文章中舉了幾個疑惑重重的例子,看看用上面學到的知識能不能合理地解釋。
# 例 0
def foo():
    exec('y = 1 + 1')
    z = locals()['y']
    print(z)

foo()
# 輸出:2


# 例 1
def foo():
    exec('y = 1 + 1')
    y = locals()['y']
    print(y)

foo()
# 報錯:KeyError: 'y'

以上是貓哥文章中舉的示例,首先例 0 很簡單,因為 exec 影響了所在的局部名字空間,里面存在 "y": 2 這個鍵值對,所以 locals()["y"] 會返回 2。

但例 1 則不同,因為 Python 在語法解析的時候發現了 y  = ... 這樣的賦值語句,那么它在編譯的時候就知道函數里面存在 y 這個局部變量,并寫入符號表中。既然符號表中存在,那么調用 locals 的時候就會寫入到名字空間中。但問題是變量 y 的值是多少呢?由于對 y 賦值是發生在調用 locals 之后,所以在調用 locals 的時候,y 的值還是一個 NULL,也就是變量還沒有賦值。所以會將名字空間中的 "y": 2 這個鍵值對給刪掉,于是報出 KeyError 錯誤。

再來看看貓哥文章的例 2:

# 例 2
def foo():
    y = 1 + 1
    y = locals()['y']
    print(y)

foo()
# 2

locals() 是對真實存在的局部變量的一個拷貝,在調用 locals 之前 y 就已經創建好了。符號表里面有 "y",數組 f_localsplus 里面有數值 2,所以調用 locals() 的時候,會得到 {"y": 2},因此函數執行正常。

貓哥文章的例 3:

# 例3
def foo():
    exec('y = 1 + 1')
    boc = locals()
    y = boc['y']
    print(y)

foo()
# KeyError: 'y'

這個例3 和例 1 是一樣的,只不過用變量 boc 將局部名字空間保存起來了。執行 exec 的時候,會創建局部名字空間,寫入鍵值對 "y": 2。

但調用 locals 的時候,發現函數內部存在局部變量 y 并且還尚未賦值,于是又會將 "y": 2 這個鍵值對給刪掉,因此 boc 變成了一個空字典。于是執行 y = boc["y"] 的時候會出現 KeyError。

貓哥文章的例 4:

# 例4
def foo():
    boc = locals()
    exec('y = 1 + 1')
    y = boc['y']
    print(y)

foo()
# 2

顯然在調用 locals 的時候,會返回一個空字典,因為此時的局部變量都還沒有賦值。但需要注意的是:boc 已經指向了局部名字空間(字典),而局部名字空間在一個函數里面也是唯一的。

然后執行 exec("y = 1 + 1"),會往局部名字空間中寫入一個鍵值對,而變量 boc 指向的字典也會發生改變,因為是同一個字典,所以程序正常執行。

貓哥文章的例 5:

# 例5
def foo():
    boc = locals()
    exec('y = 1 + 1')
    print(locals())
    y = boc['y']
    print(y)

foo()

# {'boc': {...}} 
# KeyError: 'y'

首先在執行 boc = locals() 之后,boc 會指向一個空字典,然后 exec 函數執行之后會往字典里面寫入一個鍵值對 "y": 2。如果在 exec 執行之后,直接執行 y = boc["y"],那么代碼是沒有問題的,但問題是在中間插入了一個 print(locals())。

我們說過,當調用 locals 的時候,會對名字空間進行更新,然后返回更新之后的名字空間。由于函數內部存在 y = ... 這樣的賦值語句,所以符號表中就存在 "y" 這個符號,于是會進行更新。但更新的時候,發現 y 還沒有被賦值,于是又將字典中的鍵值對 "y": 2 給刪掉了。

由于局部名字空間只有一份,所以 boc 指向的字典也會發生改變,換句話說在 print(locals()) 之后,boc 就指向了一個空字典,因此出現 KeyError。

小結

以上我們就探討了局部變量的存儲原理以及它和 local 名字空間的關系。

  • 局部變量在編譯時就已經確定,所以會采用數組靜態存儲,并且在整個作用域內都是可見的。
  • f_localsplus 的內存被分成了四份,局部變量的值便存儲在第一份空間中。
  • 局部名字空間是對真實存在的局部變量的拷貝,調用 locals() 時,會遍歷得到每一個符號和與之綁定的值,然后拷貝到局部名字空間。
  • 如果遍歷時發現變量值為 NULL,這就說明獲取名字空間時,該變量尚未賦值,那么要將它從名字空間中刪掉。
責任編輯:武曉燕 來源: 古明地覺的編程教室
相關推薦

2024-07-09 08:35:09

2024-05-22 08:02:30

2024-09-20 14:46:49

Python函數編譯

2018-05-14 09:15:24

Python變量函數

2012-07-11 23:10:49

SQL Server數據庫

2009-09-17 13:05:38

Linq局部變量類型

2015-09-18 13:08:36

更新RedstoneWindows 10

2020-11-11 21:26:48

函數變量

2009-08-26 16:37:07

C#迭代器局部變量

2010-03-15 09:32:56

Python函數

2024-05-08 08:38:02

Python變量對象

2024-05-29 08:49:22

Python全局變量局部變量

2015-01-07 14:41:32

Android全局變量局部變量

2023-03-26 00:04:14

2009-09-11 10:07:05

Linq隱式類型化局部

2009-10-12 14:13:00

VB.NET使用局部變

2009-12-15 10:48:54

Ruby局部變量

2020-10-26 07:07:50

線程安全框架

2024-10-14 11:14:38

Python變量靜態

2017-02-08 12:28:37

Android變量總結
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 天天干夜夜操 | 国产在线精品免费 | 亚洲三级av | 国产色网 | 成人乱人乱一区二区三区软件 | 亚洲一一在线 | 欧美日韩精品免费 | 色综合成人网 | 九九热这里 | 色综合一区 | 中文字幕免费在线 | 91日韩在线 | 在线日韩| 综合久久久久久久 | 亚洲免费视频网址 | 91精品国产91综合久久蜜臀 | 国产91网站在线观看 | 久久久久久免费毛片精品 | 日韩av福利在线观看 | 久热爱 | 欧美日韩一 | 91久久久久久久久久久 | 午夜影视免费片在线观看 | 中文天堂在线一区 | 四虎影院美女 | 一呦二呦三呦国产精品 | 在线成人一区 | 午夜激情免费视频 | 青青草原综合久久大伊人精品 | 国产在线不卡视频 | 黑人巨大精品 | 久久99精品久久久久子伦 | 亚洲自拍一区在线观看 | 亚洲va欧美va天堂v国产综合 | 国产精品亚洲二区 | 桃花av在线 | 国产在线精品一区二区 | 亚洲 欧美 综合 | 国产毛片久久久 | 日韩精品在线免费观看视频 | 日韩精品激情 |