详解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()可读可写
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。