美文网首页
Python locals() 的陷阱

Python locals() 的陷阱

作者: 51reboot | 来源:发表于2019-02-28 15:01 被阅读11次

    在工作中, 有时候会遇到一种情况: 动态地进行变量赋值, 不管是局部变量还是全局变量, 在我们绞尽脑汁的时候, 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 <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 异常?

    再看一个例子:

    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
    

    上下两段代码, 区别就是, 下面的有显示赋值的代码, 虽然也是同样触发了 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() 实际上做了下面几件事:

    1. 判断帧对象 的 f_f->f_locals 是否为空, 若是, 则新建一个字典对象.
    2. 分别将 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() 可读可写

    作者:Lin_R
    出处:http://t.cn/RHki2rr

    51Reboot 2019 最新课程招生信息
    Python 零基础入门课程
    此课程为面授班和网络班,一共 15 个课时,每周上一个全天,历时4个月。附加:录播视频+笔记+答疑2019-6月份开课

    Python 自动化运维进阶课程
    此课程为面授班和网络班,一共 15 个课时,每周上一个全天,历时4个月。附加:录播视频+笔记+答疑2019-4月份开课

    Docker+K8s 课程
    此课程为网络班,一共 150个课时,每周上一个全天,历时4个月。附加:录播视频+笔记+答疑现已开课
    现在报名即可享受早鸟价

    您可以添加我们的小助手WeChat:17812796384 咨询。


    123.jpg

    相关文章

      网友评论

          本文标题:Python locals() 的陷阱

          本文链接:https://www.haomeiwen.com/subject/gsnhuqtx.html