2021-01-18 Python locals() 的陷阱

在工作中, 有時候會遇到一種情況: 動態(tài)地進行變量賦值, 不管是局部變量還是全局變量, 在我們絞盡腦汁的時候, Python已經(jīng)為我們解決了這個問題.

Python的命名空間通過一種字典的形式來體現(xiàn), 而具體到函數(shù)也就是locals() 和 globals(), 分別對應(yīng)著局部命名空間和全局命名空間. 于是, 我們也就能通過這些方法去實現(xiàn)我們"動態(tài)賦值"的需求.

例如:

def test():
    globals()['a2'] = 4
test()
print a2   

輸出 4

很自然, 既然 globals能改變?nèi)置臻g, 那理所當(dāng)然locals應(yīng)該也能修改局部命名空間.修改函數(shù)內(nèi)的局部變量.

但事實真是如此嗎? 不是!

def aaaa():
    print locals()
    for i in ['a', 'b', 'c']:
        locals()[i] = 1
    print locals()
    print a
aaaa()

輸出:
{}
{'i': 'c', 'a': 1, 'c': 1, 'b': 1}
Traceback (most recent call last):
File "5.py", line 17, in <module>
aaaa()
File "5.py", line 16, in aaaa
print a
NameError: global name 'a' is not defined

程序運行報錯了!

但是在第二次print locals()很清楚能夠看到, 局部空間是已經(jīng)有那些變量了, 其中也有變量a并且值也為1, 但是為什么到了print a卻報出NameError異常?

再看一個例子:

def aaaa():
    print locals()
    s = 'test'                    # 加入顯示賦值 s       
    for i in ['a', 'b', 'c']:
        locals()[i] = 1
    print locals()
    print s                       # 打印局部變量 s 
    print a
aaaa()

輸出:
{}
{'i': 'c', 'a': 1, 's': 'test', 'b': 1, 'c': 1}
test
Traceback (most recent call last):
File "5.py", line 19, in <module>
aaaa()
File "5.py", line 18, in aaaa
print a
NameError: global name 'a' is not defined

上下兩段代碼, 區(qū)別就是, 下面的有顯示賦值的代碼, 雖然也是同樣觸發(fā)了NameError異常, 但是局部變量s的值被打印了出來.

這就讓我們覺得很納悶, 難道通過locals()改變局部變量, 和直接賦值有不同? 想解決這個問題, 只能去看程序運行的真相了, 又得上大殺器dis~

根源探討
直接對第二段代碼解析:

13           0 LOAD_GLOBAL              0 (locals)
              3 CALL_FUNCTION            0
              6 PRINT_ITEM
              7 PRINT_NEWLINE

 14           8 LOAD_CONST               1 ('test')
             11 STORE_FAST               0 (s)

 15          14 SETUP_LOOP              36 (to 53)
             17 LOAD_CONST               2 ('a')
             20 LOAD_CONST               3 ('b')
             23 LOAD_CONST               4 ('c')
             26 BUILD_LIST               3
             29 GET_ITER
        >>   30 FOR_ITER                19 (to 52)
             33 STORE_FAST               1 (i)

 16          36 LOAD_CONST               5 (1)
             39 LOAD_GLOBAL              0 (locals)
             42 CALL_FUNCTION            0
             45 LOAD_FAST                1 (i)
             48 STORE_SUBSCR
             49 JUMP_ABSOLUTE           30
        >>   52 POP_BLOCK

 17     >>   53 LOAD_GLOBAL              0 (locals)
             56 CALL_FUNCTION            0
             59 PRINT_ITEM
             60 PRINT_NEWLINE

 18          61 LOAD_FAST                0 (s)
             64 PRINT_ITEM
             65 PRINT_NEWLINE

 19          66 LOAD_GLOBAL              1 (a)
             69 PRINT_ITEM
             70 PRINT_NEWLINE
             71 LOAD_CONST               0 (None)
             74 RETURN_VALUE
None

在上面的字節(jié)碼可以看到:

locals() 對應(yīng)的字節(jié)碼是: LOAD_GLOBAL
s='test' 對應(yīng)的字節(jié)碼是: LOAD_CONST 和 STORE_FAST
print s 對應(yīng)的字節(jié)碼是: LOAD_FAST
print a 對應(yīng)的字節(jié)碼是: LOAD_GLOBAL

從上面羅列出來的幾個關(guān)鍵語句的字節(jié)碼可以看出, 直接賦值/讀取 和 通過locals()賦值/讀取 本質(zhì)是很大不同的. 那么觸發(fā)NameError異常, 是否證明通過 locals()[i] = 1 存儲的值, 和真正的局部命名空間 是不同的兩個位置?

想要回答這個問題, 我們得先確定一個東西, 就是真正的局部命名空間如何獲取? 其實這個問題, 在上面的字節(jié)碼上, 已經(jīng)給出了標準答案了!

真正的局部命名空間, 其實是存在 STORE_FAST 這個對應(yīng)的數(shù)據(jù)結(jié)構(gòu)里面. 這個是什么鬼, 這個需要源碼來解答:

// ceval.c  從上往下, 依次是相應(yīng)函數(shù)或者變量的定義
// 指令源碼
TARGET(STORE_FAST)
{
    v = POP();
    SETLOCAL(oparg, v);
    FAST_DISPATCH();
}
--------------------
// SETLOCAL 宏定義      
#define SETLOCAL(i, value)      do { PyObject *tmp = GETLOCAL(i); \
                                     GETLOCAL(i) = value; \
                                     Py_XDECREF(tmp); } while (0)
-------------------- 
// GETLOCAL 宏定義                                    
#define GETLOCAL(i)     (fastlocals[i])     

-------------------- 
// fastlocals 真面目
PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag){
    // 省略其他無關(guān)代碼
   fastlocals = f->f_localsplus;
....
}

看到這里, 應(yīng)該就能明確了, 函數(shù)內(nèi)部的局部命名空間, 實際是就是幀對象的f的成員f_localsplus, 這是一個數(shù)組, 了解函數(shù)創(chuàng)建的童鞋可能會比較清楚, 在CALL_FUNCTION時, 會對這個數(shù)組進行初始化, 將形參賦值什么都會按序塞進去, 在字節(jié)碼 18 61 LOAD_FAST 0 (s)中, 第四列的0, 就是將f_localsplus第 0 個成員取出來, 也就是值 "s".

所以STORE_FAST才是真正的將變量存入局部命名空間, 那locals()又是什么鬼? 為什么看起來就跟真的一樣?

這個就需要分析locals, 對于這個, 字節(jié)碼可能起不了作用, 直接去看內(nèi)置函數(shù)如何定義的吧:

// bltinmodule.c
static PyMethodDef builtin_methods[] = {
    ...
    // 找到 locals 函數(shù)對應(yīng)的內(nèi)置函數(shù)是 builtin_locals 
    {"locals",          (PyCFunction)builtin_locals,     METH_NOARGS, locals_doc},
    ...
}

-----------------------------

// builtin_locals 的定義
static PyObject *
builtin_locals(PyObject *self)
{
    PyObject *d;

    d = PyEval_GetLocals();
    Py_XINCREF(d);
    return d;
}
-----------------------------

PyObject *
PyEval_GetLocals(void)
{
    PyFrameObject *current_frame = PyEval_GetFrame();  // 獲取當(dāng)前堆棧對象
    if (current_frame == NULL)
        return NULL;
    PyFrame_FastToLocals(current_frame); // 初始化和填充 f_locals
    return current_frame->f_locals;
}
-----------------------------

// 初始化和填充 f_locals 的具體實現(xiàn)
void
PyFrame_FastToLocals(PyFrameObject *f)
{
    /* Merge fast locals into f->f_locals */
    PyObject *locals, *map;
    PyObject **fast;
    PyObject *error_type, *error_value, *error_traceback;
    PyCodeObject *co;
    Py_ssize_t j;
    int ncells, nfreevars;
    if (f == NULL)
        return;
    locals = f->f_locals;
    
    // 如果locals為空, 就新建一個字典對象
    if (locals == NULL) {
        locals = f->f_locals = PyDict_New();  
        if (locals == NULL) {
            PyErr_Clear(); /* Can't report it :-( */
            return;
        }
    }
    
    co = f->f_code;
    map = co->co_varnames;
    if (!PyTuple_Check(map))
        return;
    PyErr_Fetch(&error_type, &error_value, &error_traceback);
    fast = f->f_localsplus;
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
        
    // 將 f_localsplus 寫入 locals
    if (co->co_nlocals)
        map_to_dict(map, j, locals, fast, 0);
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        // 將 co_cellvars 寫入 locals
        map_to_dict(co->co_cellvars, ncells,
                    locals, fast + co->co_nlocals, 1);
                    
        if (co->co_flags & CO_OPTIMIZED) {
            // 將 co_freevars 寫入 locals
            map_to_dict(co->co_freevars, nfreevars,
                        locals, fast + co->co_nlocals + ncells, 1);
        }
    }
    PyErr_Restore(error_type, error_value, error_traceback);
}

從上面PyFrame_FastToLocals已經(jīng)看出來, locals() 實際上做了下面幾件事:

判斷幀對象 的 f_f->f_locals 是否為空, 若是, 則新建一個字典對象.
分別將 localsplus, co_cellvars 和 co_freevars 寫入 f_f->f_locals.

在這簡單介紹下上面幾個分別是什么鬼:

localsplus: 函數(shù)參數(shù)(位置參數(shù)+關(guān)鍵字參數(shù)), 顯示賦值的變量.
co_cellvars 和 co_freevars: 閉包函數(shù)會用到的局部變量.
結(jié)論

通過上面的源碼, 我們已經(jīng)很明確知道locals() 看到的, 的確是函數(shù)的局部命名空間的內(nèi)容, 但是它本身不能代表局部命名空間, 這就好像一個代理, 它收集了A, B, C的東西, 展示給我看, 但是我卻不能簡單的通過改變這個代理, 來改變A, B, C真正擁有的東西!

這也就是為什么, 當(dāng)我們通過locals()[i] = 1的方式去動態(tài)賦值時, print a卻觸發(fā)了NameError異常, 而相反的, globals()確實真正的全局命名空間, 所以一般會說:

locals() 只讀, globals() 可讀可寫

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末村刨,一起剝皮案震驚了整個濱河市碟狞,隨后出現(xiàn)的幾起案子矫夯,更是在濱河造成了極大的恐慌殿遂,老刑警劉巖涣楷,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凿将,死亡現(xiàn)場離奇詭異此叠,居然都是意外死亡,警方通過查閱死者的電腦和手機仁讨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門羽莺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洞豁,你說我怎么就攤上這事禽翼。” “怎么了族跛?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵闰挡,是天一觀的道長。 經(jīng)常有香客問我礁哄,道長长酗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任桐绒,我火速辦了婚禮夺脾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茉继。我一直安慰自己咧叭,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布烁竭。 她就那樣靜靜地躺著菲茬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪派撕。 梳的紋絲不亂的頭發(fā)上婉弹,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天,我揣著相機與錄音终吼,去河邊找鬼镀赌。 笑死,一個胖子當(dāng)著我的面吹牛际跪,可吹牛的內(nèi)容都是我干的商佛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼姆打,長吁一口氣:“原來是場噩夢啊……” “哼良姆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起穴肘,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤歇盼,失蹤者是張志新(化名)和其女友劉穎舔痕,沒想到半個月后评抚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體豹缀,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年慨代,在試婚紗的時候發(fā)現(xiàn)自己被綠了邢笙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡侍匙,死狀恐怖氮惯,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情想暗,我是刑警寧澤妇汗,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站说莫,受9級特大地震影響杨箭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜储狭,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一互婿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辽狈,春花似錦慈参、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至着茸,卻和暖如春僧凤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背元扔。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工躯保, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人澎语。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓途事,卻偏偏與公主長得像,于是被迫代替她去往敵國和親擅羞。 傳聞我的和親對象是個殘疾皇子尸变,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容