Python locals() 的陷阱

在工做中, 有時候會遇到一種狀況: 動態地進行變量賦值, 無論是局部變量仍是全局變量, 在咱們絞盡腦汁的時候, Python已經爲咱們解決了這個問題.segmentfault

Python的命名空間經過一種字典的形式來體現, 而具體到函數也就是locals()globals(), 分別對應着局部命名空間和全局命名空間. 因而, 咱們也就能經過這些方法去實現咱們"動態賦值"的需求.數組

例如:數據結構

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

很天然, 既然 globals能改變全局命名空間, 那理所固然locals應該也能修改局部命名空間.修改函數內的局部變量. 閉包

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

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

輸出:ui

{}
{'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()很清楚可以看到, 局部空間是已經有那些變量了, 其中也有變量a而且值也爲1, 可是爲何到了print a卻報出NameError異常?code

再看一個例子:對象

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

輸出:get

{}
{'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

上下兩段代碼, 區別就是, 下面的有顯示賦值的代碼, 雖然也是一樣觸發了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

在上面的字節碼能夠看到:

  1. locals() 對應的字節碼是: LOAD_GLOBAL
  2. s='test' 對應的字節碼是: LOAD_CONSTSTORE_FAST
  3. print s 對應的字節碼是: LOAD_FAST
  4. print a 對應的字節碼是: LOAD_GLOBAL

從上面羅列出來的幾個關鍵語句的字節碼能夠看出, 直接賦值/讀取 和 經過locals()賦值/讀取 本質是很大不一樣的. 那麼觸發NameError異常, 是否證實經過 locals()[i] = 1 存儲的值, 和真正的局部命名空間 是不一樣的兩個位置?

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

真正的局部命名空間, 實際上是存在 STORE_FAST 這個對應的數據結構裏面. 這個是什麼鬼, 這個須要源碼來解答:

// ceval.c  從上往下, 依次是相應函數或者變量的定義
// 指令源碼
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){
    // 省略其餘無關代碼
   fastlocals = f->f_localsplus;
....
}

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

因此STORE_FAST纔是真正的將變量存入局部命名空間, 那locals()又是什麼鬼? 爲何看起來就跟真的同樣?

這個就須要分析locals, 對於這個, 字節碼可能起不了做用, 直接去看內置函數如何定義的吧:

// bltinmodule.c
static PyMethodDef builtin_methods[] = {
    ...
    // 找到 locals 函數對應的內置函數是 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();  // 獲取當前堆棧對象
    if (current_frame == NULL)
        return NULL;
    PyFrame_FastToLocals(current_frame); // 初始化和填充 f_locals
    return current_frame->f_locals;
}
-----------------------------

// 初始化和填充 f_locals 的具體實現
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已經看出來, locals() 實際上作了下面幾件事:

  1. 判斷幀對象 的 f_f->f_locals 是否爲空, 如果, 則新建一個字典對象.
  2. 分別將 localsplus, co_cellvarsco_freevars 寫入 f_f->f_locals.

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

  1. localsplus: 函數參數(位置參數+關鍵字參數), 顯示賦值的變量.
  2. co_cellvarsco_freevars: 閉包函數會用到的局部變量.

結論

經過上面的源碼, 咱們已經很明確知道locals() 看到的, 的確是函數的局部命名空間的內容, 可是它自己不能表明局部命名空間, 這就好像一個代理, 它收集了A, B, C的東西, 展現給我看, 可是我卻不能簡單的經過改變這個代理, 來改變A, B, C真正擁有的東西!

這也就是爲何, 當咱們經過locals()[i] = 1的方式去動態賦值時, print a卻觸發了NameError異常, 而相反的, globals()確實真正的全局命名空間, 因此通常會說

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

歡迎各位大神指點交流, QQ討論羣: 258498217
轉載請註明來源: https://segmentfault.com/a/11...

相關文章
相關標籤/搜索