Python locals() 的陷阱

在工作中, 有时候会遇到一种情况: 动态地进行变量赋值, 不管是局部变量还是全局变量, 在我们绞尽脑汁的时候, Python已经为我们解决了这个问题.

Python的命名空间通过一种字典的形式来体现, 而具体到函数也就是locals() 和 globals(), 分别对应着局部命名空间和全局命名空间. 于是, 我们也就能通过这些方法去实现我们"动态赋值"的需求.

例如:

def test():
    globals()['a2'] = 4test()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 aNameError: 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 aNameError: 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_CONST 和 STORE_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.cstatic 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. 分别将 localsplusco_cellvars 和 co_freevars 写入 f_f->f_locals.

在这简单介绍下上面几个分别是什么鬼:

  1. localsplus: 函数参数(位置参数+关键字参数), 显示赋值的变量.
  2. co_cellvars 和 co_freevars: 闭包函数会用到的局部变量.

结论

通过上面的源码, 我们已经很明确知道locals() 看到的, 的确是函数的局部命名空间的内容, 但是它本身不能代表局部命名空间, 这就好像一个代理, 它收集了A, B, C的东西, 展示给我看, 但是我却不能简单的通过改变这个代理, 来改变A, B, C真正拥有的东西!

这也就是为什么, 当我们通过locals()[i] = 1的方式去动态赋值时, print a却触发了NameError异常, 而相反的, globals()确实真正的全局命名空间, 所以一般会说

locals() 只读, globals() 可读可写

欢迎各位大神指点交流, QQ技术群: 238757010
来源: https://segmentfault.com/a/1190000012724861

原文地址:http://blog.51cto.com/51reboot/2122441

时间: 2024-08-01 19:16:46

Python locals() 的陷阱的相关文章

麦子学院—python的潜在陷阱

本文主要是为新手介绍python的潜在陷阱,现在依次看看有哪些潜在的 Python 陷阱:推荐:Python网络编程实战视频(http://www.maiziedu.com/course/python/459-5958/)不知道Python版本这是一个在StackOverflow上反复出现的问题.许多人能写出在某个版本上完美工作的代码,但在他们在自己的系统上安装有不同版本的Python.要确保你知道你正在使用的Python版本.你可以通过下边的代码查看Python版本:$ python--ver

python locals globals

Python有两个内置的函数,locals() 和globals(),它们提供了基于字典的访问局部和全局变量的方式. 首先,是关于名字空间的一个名词解释.是枯燥,但是很重要,所以要耐心些.Python使用叫做名字空间的东西来记录变量的轨迹.名字空间只是一个 字典,它的键字就是变量名,字典的值就是那些变量的值.实际上,名字空间可以象Python的字典一样进行访问,一会我们就会看到. 在一个Python程序中的任何一个地方,都存在几个可用的名字空间.每个函数都有着自已的名字空间,叫做局部名字空间,它

[py]Python locals() 函数

Python locals() 函数作用 http://www.runoob.com/python/python-func-locals.html locals() 函数会以字典类型返回当前位置的全部局部变量. 对于函数, 方法, lambda 函式, 类, 以及实现了 call 方法的类实例, 它都返回 True. locals()实例 >>>def runoob(arg): # 两个局部变量:arg.z ... z = 1 ... print (locals()) ... >&

多线程C调用python api的陷阱

众所周知,用脚本语言编写的服务(wsgi接口)都需要一个server容器,常见的如php的php-fpm, lightd等.python中一般是用的uwsgi,uwsgi是在wsgi的基础上的一种新的协议,可以用来部署python等脚本程序的运行.然而在不熟悉uwsgi的代码架构和c调用python的api情况下进行开发可能会遇到一些意想不到的问题. 我们先看一段代码,下面这段代码是用的Flask框架,每次请求的时候会把COUNT的值先减一再加一,最后再乘二.如果请求50次,其最终的结果应该是2

python路上的陷阱

preface: 这是个悲伤的故事.python基础没学好,容我写篇博客压压惊,不多说了,直接上代码. 一.复制运算,连等会让所有对象共享一个地址.还是不要偷懒的好. In [4]: a = b = c = {} In [5]: a["abc"] = 2 In [6]: b Out[6]: {'abc': 2} In [7]: a = {} In [8]: b = {} In [9]: c = {} In [10]: a["abc"] = 2 In [11]: b

Python的作用域陷阱

问题发现:for循环内部的"局部"变量,在出现异常后,赋的变量仍有值可以输出. 每个循环都输出,发现没有重复. 结论:无for内的"局部"变量. Python内的变量作用域,最小是以函数为单位. 之上是类,再是模块. 对于本问题,而输出没有重复,是因为出现异常后就跳出了赋值语句块,输出的设计不好. 实际上输出的值,是上次循环中赋的值.

十条Python面试题陷阱,看看你是否会中招

问题1:请问如何修改以下Python代码,使得下面的代码调用类A的show方法? 答:这道题的考点是类继承,只要通过__class__ 方法指定类对象就可以了.补充的代码如下: 问题2:请问如何修改以下Python代码,使得代码能够运行? 答:此题考察得是方法对象,为了能让对象实例能被直接调用,需要实现 __call__ 方法,补充代码如下: 问题3:下面这段代码的输出是什么? 答: 此题考察的是new和init的用法,使用 __new__ 方法,可以决定返回那个对象,也就是创建对象之前调用的,

python默认参数陷阱

对于学习python的人都有这样的困惑 def foo(a=[]): a.append(5) return a Python新手希望这个函数总是返回一个只包含一个元素的列表:[5].结果却非常不同,而且非常惊人(对于新手来说): >>> foo() [5] >>> foo() [5, 5] >>> foo() [5, 5, 5] >>> foo() [5, 5, 5, 5] >>> foo() 实际上,这不是设计缺陷

[python] 之 函数陷阱

一.本地变量是静态检测的 在函数中进行分配的变量名是默认为本地变量的(遵循LEGB法则),它们存在于函数的作用域中,只在函数运行时存在,函数运行结束,变量消失. 二.默认的可变对象 可变类型的默认参数在函数调用之间保存了他们的状态 三.没有return语句的函数,返回None 1 def saver(x=[]): 2 x.append(1) 3 print(x) 4 5 print (saver([2])) 6 print (saver()) 7 print (saver()) 8 print