Python動態賦值的陷阱分析

發布: 2019-03-25 10:06:03
轉載
2463 人瀏覽過

這篇文章帶給大家的內容是關於Python動態賦值的陷阱分析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

命名空間與作用域問題,看似微不足道,其實背後大有文章。

由於篇幅所限,還有一個重要的知識內容沒談,即「locals() 與 globals() 的讀寫問題」。之所以說這個問題很重要,是因為它可以實現一些靈活的動態賦值的功能。

它們都是字典類型,用法不需多言。然而,在使用過程中,有一個陷阱需要注意:globals() 可讀可寫,而 locals() 只可讀卻不可寫。今天分享的文章,就是在探究這個問題,寫得很深入,特地分享給大家。

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

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()
登入後複製

輸出:

{}
{'i': 'c', 'a': 1, 'c': 1, 'b': 1}
Traceback (most recent call last):
  File "5.py", line 17, in 
    aaaa()
  File "5.py", line 16, in aaaa
    print a
NameError: global name 'a' is not defined
登入後複製

程式運行報錯了!

但是在第二次print locals()很清楚能夠看到, 局部空間是已經有那些變數了, 其中也有變數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 
    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
登入後複製

在上面的字節碼可以看到:

locals()對應的位元組碼是: LOAD_GLOBAL

s='test'對應的位元組碼是: LOAD_CONST 和 STORE_FAST

print s對應的位元組碼是: LOAD_FAST

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() 實際上做了下面幾件事:

判斷幀物件的 f_f->f_locals是否為空, 若是,則新建一個字典物件.

分別將localsplus, co_cellvars和co_freevars 寫入 f_f->f_locals.

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

localsplus: 函數參數(位置參數關鍵字參數), 顯示賦值的變數.

co_cellvars 與 co_freevars: 閉包函數會用到的局部變數.

結論

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

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

這篇文章到這裡就已經全部結束了,更多其他精彩內容可以關注PHP中文網的python影片教學專欄!

以上是Python動態賦值的陷阱分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:微信公众号:python猫
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!