自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

局部變量是怎么實(shí)現(xiàn)靜態(tài)查找的,它和 local 名字空間又有什么聯(lián)系呢?

開發(fā) 前端
我們說過,當(dāng)調(diào)用 locals 的時(shí)候,會(huì)對(duì)名字空間進(jìn)行更新,然后返回更新之后的名字空間。由于函數(shù)內(nèi)部存在 y = ... 這樣的賦值語句,所以符號(hào)表中就存在 "y" 這個(gè)符號(hào),于是會(huì)進(jìn)行更新。但更新的時(shí)候,發(fā)現(xiàn) y 還沒有被賦值,于是又將字典中的鍵值對(duì) "y": 2 給刪掉了。


楔子


前面我們剖析了字節(jié)碼的執(zhí)行流程,本來應(yīng)該接著介紹一些常見指令的,但因?yàn)橛袔讉€(gè)指令涉及到了局部變量,所以我們單獨(dú)拿出來說。與此同時(shí),我們還要再度考察一下 local 名字空間,它的背后還隱藏了很多內(nèi)容。

我們知道函數(shù)的參數(shù)和函數(shù)內(nèi)部定義的變量都屬于局部變量,均是通過靜態(tài)方式訪問的。

x = 123

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

# co_nlocals 會(huì)返回局部變量的個(gè)數(shù)
# 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

無論是參數(shù)還是內(nèi)部新創(chuàng)建的變量,本質(zhì)上都是局部變量。

按照之前的理解,當(dāng)訪問一個(gè)全局變量時(shí),會(huì)去訪問 global 名字空間(也叫全局名字空間)。

圖片

那么問題來了,當(dāng)操作函數(shù)的局部變量時(shí),是不是也等價(jià)于操作其內(nèi)部的 local 名字空間(局部名字空間)呢?我們往下看。


圖片


如何訪問(創(chuàng)建)一個(gè)局部變量


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

但不幸的是,虛擬機(jī)在為調(diào)用的函數(shù)創(chuàng)建棧幀對(duì)象時(shí),這個(gè)至關(guān)重要的 local 名字空間并沒有被創(chuàng)建。因?yàn)闂?f_locals 字段和 f_globals 字段分別指向了局部名字空間和全局名字空間,而創(chuàng)建棧幀時(shí) f_locals 被初始化成了 NULL,所以并沒有創(chuàng)建局部名字空間。

我們通過源碼來進(jìn)行驗(yàn)證,不過要先補(bǔ)充一個(gè)知識(shí)點(diǎn),就是當(dāng)調(diào)用一個(gè) Python 函數(shù)時(shí),底層會(huì)調(diào)用哪些 C 函數(shù)呢?

圖片

我們看一下源碼:

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


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

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

    // 棧幀創(chuàng)建之后,調(diào)用 _PyFrame_Initialize,進(jìn)行初始化
    _PyFrame_Initialize(frame, func, locals, code, 0);
    // ...
}

// Include/internal/pycore_frame.h
/*
 * 對(duì) frame 進(jìn)行初始化
 */
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 字段初始化為參數(shù) locals
    // 而參數(shù) locals 是從 _PyFunction_Vectorcall 一層層傳過來的
    // 由于 _PyFunction_Vectorcall 傳的是 NULL
    // 所以棧幀的 f_locals 字段最終會(huì)被初始化為 NULL
    frame->f_locals = locals;
    // ...
}

所以我們驗(yàn)證了在調(diào)用函數(shù)時(shí),棧幀的局部名字空間確實(shí)被初始化為 NULL,當(dāng)然也明白了 C 函數(shù)的調(diào)用鏈路。

我們用 Python 代碼演示一下:

import inspect

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


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

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

# 咦,不是說局部名字空間被初始化為 NULL 嗎?
# 那么在 Python 里面獲取的話,結(jié)果應(yīng)該是個(gè) None 才對(duì)啊
# 關(guān)于這一點(diǎn),我們稍后會(huì)解釋

總之對(duì)于函數(shù)而言,在創(chuàng)建棧幀時(shí),它的 f_locals 被初始化為 NULL。那么問題來了,局部變量到底存儲(chǔ)在什么地方呢?當(dāng)然,由于變量只是一個(gè)名字(符號(hào)),而局部變量的名字都存儲(chǔ)在符號(hào)表中,所以更嚴(yán)謹(jǐn)?shù)恼f法是,局部變量的值存儲(chǔ)在什么地方?

在介紹虛擬機(jī)執(zhí)行字節(jié)碼的時(shí)候我們說過,當(dāng)函數(shù)被調(diào)用時(shí),虛擬機(jī)會(huì)為其創(chuàng)建一個(gè)棧幀。棧幀是虛擬機(jī)的執(zhí)行環(huán)境,包含了執(zhí)行時(shí)所依賴的上下文,而棧幀內(nèi)部有一個(gè)字段叫 f_localsplus,它是一個(gè)數(shù)組。

圖片圖片

這個(gè)數(shù)組雖然是一段連續(xù)內(nèi)存,但在邏輯上被分成了 4 份,其中局部變量便存儲(chǔ)在 f_localsplus 的第一份空間中?,F(xiàn)在我們明白了,局部變量是靜態(tài)存儲(chǔ)在數(shù)組中的。

我們舉個(gè)例子。

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

它的字節(jié)碼如下:

圖片

注意里面的 LOAD_FAST 和 STORE_FAST,這兩個(gè)指令對(duì)應(yīng)的邏輯如下。

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"
    // 將值壓入運(yùn)行時(shí)棧,等價(jià)于 PUSH(value)
    STACK_GROW(1);
    stack_pointer[-1] = value;
    DISPATCH();
}

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

所以 LOAD_FAST 和 STORE_FAST 分別負(fù)責(zé)加載和創(chuàng)建局部變量,而核心就是里面的兩個(gè)宏: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)
/* 這里額外再補(bǔ)充一個(gè)關(guān)于 C 語言的知識(shí)點(diǎn)
 * 我們看到宏 SETLOCAL 展開之后的結(jié)果是 do {...} while (0) 
 * do while 循環(huán)會(huì)先執(zhí)行 do 里面的循環(huán)體,然后再判斷條件是否滿足
 * 因此從效果上來說,執(zhí)行 do {...} while (0) 和直接執(zhí)行 ... 是等價(jià)的
 * 那么問題來了,既然效果等價(jià),為啥還要再套一層 do while 呢
 * 其實(shí)原因很簡單,如果宏在展開之后會(huì)生成多條語句,那么這些語句要成為一個(gè)整體
 * 另外由于 C 程序的語句要以分號(hào)結(jié)尾,所以在調(diào)用宏時(shí),我們也會(huì)習(xí)慣性地在結(jié)尾加上分號(hào)
 * 因此我們希望有這樣一種結(jié)構(gòu),能同時(shí)滿足以下要求:
 *   1)可以將多條語句包裹起來,作為一個(gè)整體;
 *   2)程序的語義不能發(fā)生改變;
 *   3)在語法上,要以分號(hào)結(jié)尾;
 * 顯然 do while 完美滿足以上三個(gè)要求,只需將 while 里的條件設(shè)置為 0 即可
 * 并且當(dāng)編譯器看到 while (0) 時(shí),也會(huì)進(jìn)行優(yōu)化,去掉不必要的循環(huán)控制結(jié)構(gòu)
 * 因此以后看到 do {...} while (0) 時(shí),不要覺得奇怪,這是宏的一個(gè)常用技巧
 */

我們看到操作局部變量,就是在基于索引操作數(shù)組 f_localsplus,顯然這個(gè)過程比操作字典要快。盡管字典是經(jīng)過高度優(yōu)化的,但顯然再怎么優(yōu)化,也不可能快過數(shù)組的靜態(tài)操作。

所以此時(shí)我們對(duì)局部變量的藏身之處已經(jīng)了然于心,它們就存放在棧幀的 f_localsplus 字段中,而之所以沒有使用 local 名字空間的原因也很簡單。因?yàn)楹瘮?shù)內(nèi)部的局部變量在編譯時(shí)就已經(jīng)確定了,個(gè)數(shù)是不會(huì)變的,因此編譯時(shí)也能確定局部變量占用的內(nèi)存大小,以及訪問局部變量的字節(jié)碼指令應(yīng)該如何訪問內(nèi)存。

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

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

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

  • 當(dāng)創(chuàng)建變量 c 時(shí),只需修改數(shù)組 f_localsplus 中索引為 2 的元素即可。
  • 當(dāng)訪問變量 c 時(shí),只需獲取數(shù)組 f_localsplus 中索引為 2 的元素即可。

這個(gè)過程是基于數(shù)組索引實(shí)現(xiàn)的靜態(tài)查找,所以操作局部變量和操作全局變量有著異曲同工之妙。操作全局變量本質(zhì)上是基于 key 操作字典的 value,其中 key 是變量的名稱,value 是變量的值;而操作局部變量本質(zhì)上是基于索引操作數(shù)組 f_localsplus 的元素,這個(gè)索引就是變量名在符號(hào)表中的索引,對(duì)應(yīng)的數(shù)組元素就是變量的值。

所以我們說 Python 的變量其實(shí)就是個(gè)名字,或者說符號(hào),到這里是不是更加深刻地感受到了呢?

但對(duì)于局部變量來說,如果想實(shí)現(xiàn)靜態(tài)查找,顯然要滿足一個(gè)前提:變量名在符號(hào)表中的索引和與之綁定的值在 f_localsplus 中的索引必須是一致的。毫無疑問,兩者肯定是一致的,并且索引是多少在編譯階段便已經(jīng)確定,會(huì)作為指令參數(shù)保存在字節(jié)碼指令序列中。

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


圖片


解密 local 名字空間


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

x = 1

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

global 空間全局唯一,在 Python 層面上就是一個(gè)字典,在任何地方操作該字典,都相當(dāng)于操作全局變量,即使是在函數(shù)內(nèi)部。

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

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


foo()  # 1

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

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

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() 居然也會(huì)顯示內(nèi)部的局部變量,相信聰明如你已經(jīng)猜到 locals() 是怎么回事了。因?yàn)榫植孔兞坎皇菑木植棵挚臻g里面查找的,所以它初始為空,但當(dāng)我們執(zhí)行 locals() 的時(shí)候,會(huì)動(dòng)態(tài)構(gòu)建一個(gè)字典出來。

符號(hào)表里面存儲(chǔ)了局部變量的符號(hào)(或者說名字),f_localsplus 里面存儲(chǔ)了局部變量的值,當(dāng)執(zhí)行 locals() 的時(shí)候,會(huì)基于符號(hào)表和 f_localsplus 創(chuàng)建一個(gè)字典出來。

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

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

我們來看一下變量的創(chuàng)建。

  • 由于符號(hào) name 位于符號(hào)表中索引為 0 的位置,那么執(zhí)行 name = "satori" 時(shí),就會(huì)將 "satori" 放在 f_localsplus 中索引為 0 的位置。
  • 由于符號(hào) age 位于符號(hào)表中索引為 1 的位置,那么執(zhí)行 age = 17 時(shí),就會(huì)將 17 放在 f_localsplus 中索引為 1 的位置。
  • 由于符號(hào) gender 位于符號(hào)表中索引為 2 的位置,那么執(zhí)行 gender = "female" 時(shí),就會(huì)將 "female" 放在 f_localsplus 中索引為 2 的位置。

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

我們用一張圖來描述這個(gè)過程:

圖片

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

至于 locals() 或者說局部名字空間,它是基于符號(hào)表和 f_localsplus 動(dòng)態(tài)創(chuàng)建的。為了方便我們獲取已存在的局部變量,執(zhí)行 locals() 會(huì)臨時(shí)創(chuàng)建一個(gè)字典。

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

def foo():
    name = "satori"
    # 會(huì)從 f_localsplus 中靜態(tài)查找
    print(name)
    # 先基于已有的變量和值創(chuàng)建一個(gè)字典
    # 然后通過字典實(shí)現(xiàn)變量的動(dòng)態(tài)查找
    print(locals()["name"])

foo()
"""
satori
satori
"""

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

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

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

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

局部變量是靜態(tài)存儲(chǔ)在數(shù)組里的,locals() 只是做了一個(gè)拷貝而已。往局部名字空間里面添加一個(gè)鍵值對(duì),不等于創(chuàng)建一個(gè)局部變量,因?yàn)榫植孔兞坎皇菑乃@里查找的,因此代碼中打印 age 報(bào)錯(cuò)了。但如果外部還有一個(gè)全局變量 age 的話,那么會(huì)打印全局變量 age。

然后再補(bǔ)充一點(diǎn),我們說全局名字空間在任何地方都是唯一的,而對(duì)于函數(shù)而言,它的局部名字空間在整個(gè)函數(shù)內(nèi)部也是唯一的。不管調(diào)用 locals 多少次,拿到的都是同一個(gè)字典。

def foo():
    name = "satori"
    # 執(zhí)行 locals() 的時(shí)候,內(nèi)部只有一個(gè)鍵值對(duì)
    d = locals()
    print(d)  # {'name': 'satori'}
    # 再次獲取,此時(shí)有兩個(gè)鍵值對(duì)
    print(locals())  # {'name': 'satori', 'd': {...}}
    
    # 但兩者的 id 相同,因?yàn)橐粋€(gè)函數(shù)只有一個(gè)局部名字空間
    # 不管調(diào)用多少次 locals(),拿到的都是同一個(gè)字典
    print(id(d) == id(locals()))  # True

foo()

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

因此局部名字空間初始為 NULL,但在第一次執(zhí)行 locals() 時(shí),會(huì)以符號(hào)表中的符號(hào)作為 key,f_localsplus 中的值作為 value,創(chuàng)建一個(gè)字典作為函數(shù)的局部名字空間。而后續(xù)再執(zhí)行 locals() 的時(shí)候,由于名字空間已存在,就不會(huì)再次創(chuàng)建了,直接基于當(dāng)前的局部變量對(duì)字典進(jìn)行更新即可。

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

foo()

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


圖片


local 名字空間的創(chuàng)建過程


目前我們已經(jīng)知道 local 名字空間是怎么創(chuàng)建的了,也熟悉了它的特性,下面通過源碼來看一下它的構(gòu)建過程。

// Python/bltinmodule.c
static PyObject *
builtin_locals_impl(PyObject *module)
{
    // Python 內(nèi)置函數(shù)的源碼實(shí)現(xiàn)位于 bltinmodule.c 中
    // 這里又調(diào)用了 _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;
    }
    // 調(diào)用了 _PyFrame_GetLocals
    return _PyFrame_GetLocals(current_frame, 1);
}

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

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

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

    PyCodeObject *co = frame->f_code;
    // co_nlocalsplus 等于局部變量、cell 變量、free 變量的個(gè)數(shù)之和
    // 這些變量都要拷貝到 local 名字空間中
    for (int i = 0; i < co->co_nlocalsplus; i++) {
        PyObject *value;
        // 獲取 f_localsplus[i],在函數(shù)內(nèi)部會(huì)對(duì) value 進(jìn)行修改
        if (!frame_get_var(frame, co, i, &value)) {
            continue;
        }
        // f_localsplus[i] 對(duì)應(yīng)局部名字空間的 value
        // 那么 co_localsplusnames[i] 顯然對(duì)應(yīng)局部名字空間的 key
        // 估計(jì)有人已經(jīng)忘記 co_localsplusnames 字段的含義了,我們?cè)俳忉屢幌?        /* co_localsplusnames:包含所有局部變量、cell 變量、free 變量的名稱
         * co_nlocalsplus:co_localsplusnames 的長度,或者說這些變量的個(gè)數(shù)之和

         * co_varnames:包含所有局部變量的名稱,co_nlocals:局部變量的個(gè)數(shù)
         * co_cellvars:包含所有 cell 變量的名稱,co_ncellvars:cell 變量的個(gè)數(shù)
         * co_freevars:包含所有 free 變量的名稱,co_nfreevars:free 變量的個(gè)數(shù)
         
         * 因此不難得出它們之間的關(guān)系:
         * co_localsplusnames = co_varnames + co_cellvars + co_freevars
         * co_nlocalsplus = co_nlocals + co_ncellvars + co_nfreevars
         */
        // 所以 co_localsplusnames 也是符號(hào)表,并且是 co_varnames 的超集
        PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
        // 到此局部名字空間的 key 和 value 便有了,但還要做一個(gè)判斷
        _PyLocals_Kind kind = _PyLocals_GetKind(co->co_localspluskinds, i);
        // 如果變量的類型是隱藏變量,那么添加到 hidden 中
        // 所以 co_localsplusnames 其實(shí)還包含了隱藏變量的名稱
        // 但我們基本不會(huì)遇到這種情況,因此關(guān)于隱藏變量直接忽略掉即可
        if (kind & CO_FAST_HIDDEN) {
            if (include_hidden && value != NULL) {
                if (PyObject_SetItem(hidden, name, value) != 0) {
                    goto error;
                }
            }
            continue;
        }
        // 如果不是隱藏變量,那么拷貝到 locals 中,但這里有一個(gè)判斷很重要
        // 當(dāng) value 為 NULL 時(shí),如果 key 已存在,那么會(huì)將它刪掉
        // 關(guān)于這里的玄機(jī),稍后會(huì)解釋
        if (value == NULL) {
            if (PyObject_DelItem(locals, name) != 0) {
                if (PyErr_ExceptionMatches(PyExc_KeyError)) {
                    PyErr_Clear();
                }
                else {
                    goto error;
                }
            }
        }
        // 到這里說明 value 指向了一塊合法的內(nèi)存
        // 也就是變量名和變量值已經(jīng)完成了綁定,那么將它們添加到 locals 中
        else {
            if (PyObject_SetItem(locals, name, value) != 0) {
                goto error;
            }
        }
        // 繼續(xù)遍歷下一個(gè)符號(hào)
    }
    // 隱藏變量保存在 hidden 中,它不會(huì)污染 f_locals
    if (include_hidden && PyDict_Size(hidden)) {
        // 創(chuàng)建一個(gè)新字典
        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,所以返回的結(jié)果會(huì)包含 hidden 里的鍵值對(duì)
        // 但 f_locals 里面是沒有隱藏變量的
        locals = innerlocals;
    }
    else {
        Py_INCREF(locals);
    }
    Py_CLEAR(hidden);
    // 返回 locals
    return locals;

  error:
    Py_XDECREF(hidden);
    return NULL;
}

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

但里面有一處細(xì)節(jié)非常關(guān)鍵。

圖片

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


圖片


local 名字空間與 exec 函數(shù)


我們?cè)賮泶钆?exec 關(guān)鍵字,結(jié)果會(huì)更加明顯。首先 exec 函數(shù)可以將一段字符串當(dāng)成代碼來執(zhí)行,并將執(zhí)行結(jié)果體現(xiàn)在當(dāng)前的名字空間中。

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,因?yàn)樘摂M機(jī)并不知道 exec("x = 1") 是創(chuàng)建一個(gè)局部變量,它只知道這是一個(gè)函數(shù)調(diào)用。

事實(shí)上 exec 會(huì)作為一個(gè)獨(dú)立的編譯單元來執(zhí)行,并且有自己的作用域。

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

由于函數(shù) foo 對(duì)應(yīng)的 PyCodeObject 對(duì)象的符號(hào)表中并沒有 x 這個(gè)符號(hào),所以報(bào)錯(cuò)了。

補(bǔ)充:exec 默認(rèn)影響的是 local 名字空間,如果在執(zhí)行時(shí)發(fā)現(xiàn) local 名字空間為 NULL,那么會(huì)自動(dòng)創(chuàng)建一個(gè)。所以調(diào)用 exec 也可以創(chuàng)建名字空間(當(dāng)它為 NULL 時(shí))。

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

如果放在模塊里面是可以的,因?yàn)槟K的 local 名字空間和 global 名字空間指向同一個(gè)字典,所以 global 名字空間會(huì)多一個(gè) key 為 "x" 的鍵值對(duì)。而全局變量是從 global 名字空間中查找的,所以這里沒有問題。

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

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

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

以上幾個(gè)例子都比較簡單,接下來我們開始上強(qiáng)度了。

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

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

1)函數(shù)的局部變量在編譯的時(shí)候已經(jīng)確定,并存儲(chǔ)在對(duì)應(yīng)的 PyCodeObject 對(duì)象的符號(hào)表中,這是由語法規(guī)則所決定的;

2)函數(shù)內(nèi)的局部變量在其整個(gè)作用域范圍內(nèi)都是可見的;

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

因?yàn)榫植孔兞靠梢栽诤瘮?shù)的任意位置創(chuàng)建,或者修改,所以每一次執(zhí)行 locals() 的時(shí)候,都會(huì)遍歷符號(hào)表和 f_localsplus,然后組成鍵值對(duì)拷貝到名字空間中。

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

然后在更新名字空間的時(shí)候,如果發(fā)現(xiàn)值是個(gè) NULL,那么就把名字空間中該變量對(duì)應(yīng)的鍵值對(duì)給刪掉。

圖片圖片

所以 bar 函數(shù)執(zhí)行 locals()["x"] 的時(shí)候,會(huì)先獲取名字空間,原本里面是有 "x": 1 這個(gè)鍵值對(duì)的。但因?yàn)橘x值語句 x = 123 的存在,導(dǎo)致符號(hào)表里面存在 "x" 這個(gè)符號(hào),可執(zhí)行 locals() 的時(shí)候又尚未完成賦值,因此值為 NULL,于是又把這個(gè)鍵值對(duì)給刪掉了。所以執(zhí)行 locals()["x"] 的時(shí)候,出現(xiàn)了 KeyError。

因?yàn)榫植棵挚臻g體現(xiàn)的是局部變量的值,而調(diào)用 locals 的時(shí)候,局部變量 x 還沒有被創(chuàng)建。所以 locals() 里面不應(yīng)該存在 key 為 "x" 的鍵值對(duì),于是會(huì)將它刪除。

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

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

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


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

上面代碼中,局部變量的創(chuàng)建發(fā)生在 exec 之后,如果發(fā)生在 exec 之前也是類似的結(jié)果。

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") 執(zhí)行之后,名字空間也變成了 {"x": 2}。但每次調(diào)用 locals,都會(huì)對(duì)字典進(jìn)行更新,所以在 bar 函數(shù)里面獲取名字空間的時(shí)候,又把 "x" 對(duì)應(yīng)的 value 給更新回來了。

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

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

薛定諤的貓

當(dāng) Python 中混進(jìn)一只薛定諤的貓……,這是《Python 貓》在 19 年更新的一篇文章,里面探討的內(nèi)容和我們本文的主題是重疊的。貓哥在文章中舉了幾個(gè)疑惑重重的例子,看看用上面學(xué)到的知識(shí)能不能合理地解釋。
# 例 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()
# 報(bào)錯(cuò):KeyError: 'y'

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

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

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

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

foo()
# 2

locals() 是對(duì)真實(shí)存在的局部變量的一個(gè)拷貝,在調(diào)用 locals 之前 y 就已經(jīng)創(chuàng)建好了。符號(hào)表里面有 "y",數(shù)組 f_localsplus 里面有數(shù)值 2,所以調(diào)用 locals() 的時(shí)候,會(huì)得到 {"y": 2},因此函數(shù)執(zhí)行正常。

貓哥文章的例 3:

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

foo()
# KeyError: 'y'

這個(gè)例3 和例 1 是一樣的,只不過用變量 boc 將局部名字空間保存起來了。執(zhí)行 exec 的時(shí)候,會(huì)創(chuàng)建局部名字空間,寫入鍵值對(duì) "y": 2。

但調(diào)用 locals 的時(shí)候,發(fā)現(xiàn)函數(shù)內(nèi)部存在局部變量 y 并且還尚未賦值,于是又會(huì)將 "y": 2 這個(gè)鍵值對(duì)給刪掉,因此 boc 變成了一個(gè)空字典。于是執(zhí)行 y = boc["y"] 的時(shí)候會(huì)出現(xiàn) KeyError。

貓哥文章的例 4:

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

foo()
# 2

顯然在調(diào)用 locals 的時(shí)候,會(huì)返回一個(gè)空字典,因?yàn)榇藭r(shí)的局部變量都還沒有賦值。但需要注意的是:boc 已經(jīng)指向了局部名字空間(字典),而局部名字空間在一個(gè)函數(shù)里面也是唯一的。

然后執(zhí)行 exec("y = 1 + 1"),會(huì)往局部名字空間中寫入一個(gè)鍵值對(duì),而變量 boc 指向的字典也會(huì)發(fā)生改變,因?yàn)槭峭粋€(gè)字典,所以程序正常執(zhí)行。

貓哥文章的例 5:

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

foo()

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

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

我們說過,當(dāng)調(diào)用 locals 的時(shí)候,會(huì)對(duì)名字空間進(jìn)行更新,然后返回更新之后的名字空間。由于函數(shù)內(nèi)部存在 y = ... 這樣的賦值語句,所以符號(hào)表中就存在 "y" 這個(gè)符號(hào),于是會(huì)進(jìn)行更新。但更新的時(shí)候,發(fā)現(xiàn) y 還沒有被賦值,于是又將字典中的鍵值對(duì) "y": 2 給刪掉了。

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

小結(jié)

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

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

2024-07-09 08:35:09

2024-05-22 08:02:30

2024-09-20 14:46:49

Python函數(shù)編譯

2018-05-14 09:15:24

Python變量函數(shù)

2012-07-11 23:10:49

SQL Server數(shù)據(jù)庫

2009-09-17 13:05:38

Linq局部變量類型

2020-11-11 21:26:48

函數(shù)變量

2009-08-26 16:37:07

C#迭代器局部變量

2010-03-15 09:32:56

Python函數(shù)

2015-09-18 13:08:36

更新RedstoneWindows 10

2024-05-29 08:49:22

Python全局變量局部變量

2015-01-07 14:41:32

Android全局變量局部變量

2024-05-08 08:38:02

Python變量對(duì)象

2009-10-12 14:13:00

VB.NET使用局部變

2009-09-11 10:07:05

Linq隱式類型化局部

2009-12-15 10:48:54

Ruby局部變量

2023-03-26 00:04:14

2020-10-26 07:07:50

線程安全框架

2017-02-08 12:28:37

Android變量總結(jié)

2009-09-22 17:21:24

線程局部變量
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)