详解Python locals()的陷阱
在工作中,有时候会遇到一种情况:动态地进行变量赋值,不管是局部变量还是全局变量,在我们绞尽脑汁的时候,Python已经为我们解决了这个问题.
Python的命名空间通过一种字典的形式来体现,而具体到函数也就是locals()和globals(),分别对应着局部命名空间和全局命名空间.于是,我们也就能通过这些方法去实现我们"动态赋值"的需求.
例如:
deftest(): globals()['a2']=4 test() printa2#输出4
很自然,既然globals能改变全局命名空间,那理所当然locals应该也能修改局部命名空间.修改函数内的局部变量.
但事实真是如此吗?不是!
defaaaa(): printlocals() foriin['a','b','c']: locals()[i]=1 printlocals() printa aaaa()
输出:
{}
{'i':'c','a':1,'c':1,'b':1}
Traceback(mostrecentcalllast):
File"5.py",line17,in
aaaa()
File"5.py",line16,inaaaa
printa
NameError:globalname'a'isnotdefined
程序运行报错了!
但是在第二次printlocals()很清楚能够看到,局部空间是已经有那些变量了,其中也有变量a并且值也为1,但是为什么到了printa却报出NameError异常?
再看一个例子:
defaaaa(): printlocals() s='test'#加入显示赋值s foriin['a','b','c']: locals()[i]=1 printlocals() prints#打印局部变量s printa aaaa()
输出:
{}
{'i':'c','a':1,'s':'test','b':1,'c':1}
test
Traceback(mostrecentcalllast):
File"5.py",line19,in
aaaa()
File"5.py",line18,inaaaa
printa
NameError:globalname'a'isnotdefined
上下两段代码,区别就是,下面的有显示赋值的代码,虽然也是同样触发了NameError异常,但是局部变量s的值被打印了出来.
这就让我们觉得很纳闷,难道通过locals()改变局部变量,和直接赋值有不同?想解决这个问题,只能去看程序运行的真相了,又得上大杀器dis~
根源探讨
直接对第二段代码解析:
130LOAD_GLOBAL0(locals) 3CALL_FUNCTION0 6PRINT_ITEM 7PRINT_NEWLINE 148LOAD_CONST1('test') 11STORE_FAST0(s) 1514SETUP_LOOP36(to53) 17LOAD_CONST2('a') 20LOAD_CONST3('b') 23LOAD_CONST4('c') 26BUILD_LIST3 29GET_ITER >>30FOR_ITER19(to52) 33STORE_FAST1(i) 1636LOAD_CONST5(1) 39LOAD_GLOBAL0(locals) 42CALL_FUNCTION0 45LOAD_FAST1(i) 48STORE_SUBSCR 49JUMP_ABSOLUTE30 >>52POP_BLOCK 17>>53LOAD_GLOBAL0(locals) 56CALL_FUNCTION0 59PRINT_ITEM 60PRINT_NEWLINE 1861LOAD_FAST0(s) 64PRINT_ITEM 65PRINT_NEWLINE 1966LOAD_GLOBAL1(a) 69PRINT_ITEM 70PRINT_NEWLINE 71LOAD_CONST0(None) 74RETURN_VALUE None
在上面的字节码可以看到:
- locals()对应的字节码是:LOAD_GLOBAL
- s='test'对应的字节码是:LOAD_CONST和STORE_FAST
- prints对应的字节码是:LOAD_FAST
- printa对应的字节码是:LOAD_GLOBAL
从上面罗列出来的几个关键语句的字节码可以看出,直接赋值/读取和通过locals()赋值/读取本质是很大不同的.那么触发NameError异常,是否证明通过locals()[i]=1存储的值,和真正的局部命名空间是不同的两个位置?
想要回答这个问题,我们得先确定一个东西,就是真正的局部命名空间如何获取?其实这个问题,在上面的字节码上,已经给出了标准答案了!
真正的局部命名空间,其实是存在STORE_FAST这个对应的数据结构里面.这个是什么鬼,这个需要源码来解答:
//ceval.c从上往下,依次是相应函数或者变量的定义 //指令源码 TARGET(STORE_FAST) { v=POP(); SETLOCAL(oparg,v); FAST_DISPATCH(); } -------------------- //SETLOCAL宏定义 #defineSETLOCAL(i,value)do{PyObject*tmp=GETLOCAL(i);\ GETLOCAL(i)=value;\ Py_XDECREF(tmp);}while(0) -------------------- //GETLOCAL宏定义 #defineGETLOCAL(i)(fastlocals[i]) -------------------- //fastlocals真面目 PyObject*PyEval_EvalFrameEx(PyFrameObject*f,intthrowflag){ //省略其他无关代码 fastlocals=f->f_localsplus; .... }
看到这里,应该就能明确了,函数内部的局部命名空间,实际是就是帧对象的f的成员f_localsplus,这是一个数组,了解函数创建的童鞋可能会比较清楚,在CALL_FUNCTION时,会对这个数组进行初始化,将形参赋值什么都会按序塞进去,在字节码1861LOAD_FAST0(s)中,第四列的0,就是将f_localsplus第0个成员取出来,也就是值"s".
所以STORE_FAST才是真正的将变量存入局部命名空间,那locals()又是什么鬼?为什么看起来就跟真的一样?
这个就需要分析locals,对于这个,字节码可能起不了作用,直接去看内置函数如何定义的吧:
//bltinmodule.c staticPyMethodDefbuiltin_methods[]={ ... //找到locals函数对应的内置函数是builtin_locals {"locals",(PyCFunction)builtin_locals,METH_NOARGS,locals_doc}, ... } ----------------------------- //builtin_locals的定义 staticPyObject* builtin_locals(PyObject*self) { PyObject*d; d=PyEval_GetLocals(); Py_XINCREF(d); returnd; } ----------------------------- PyObject* PyEval_GetLocals(void) { PyFrameObject*current_frame=PyEval_GetFrame();//获取当前堆栈对象 if(current_frame==NULL) returnNULL; PyFrame_FastToLocals(current_frame);//初始化和填充f_locals returncurrent_frame->f_locals; } ----------------------------- //初始化和填充f_locals的具体实现 void PyFrame_FastToLocals(PyFrameObject*f) { /*Mergefastlocalsintof->f_locals*/ PyObject*locals,*map; PyObject**fast; PyObject*error_type,*error_value,*error_traceback; PyCodeObject*co; Py_ssize_tj; intncells,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'treportit:-(*/ 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的方式去动态赋值时,printa却触发了NameError异常,而相反的,globals()确实真正的全局命名空间,所以一般会说
locals()只读,globals()可读可写
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。