设为首页 收藏本站
查看: 1344|回复: 0

[经验分享] Python闭包研究

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2017-4-23 12:18:52 | 显示全部楼层 |阅读模式
  其实很早以前就想写这么一篇文章了。一直没有机会。正好今天和同事讨论Python闭包的问题,趁着没遗忘赶快记录下来。以下代码运行的Python版本是2.5。
  问题还是那个很经典的问题:如下代码会抛一个错误

def foo():
a = 1
def bar():
a = a + 1
bar()
print a
  错误则是:

UnboundLocalError: local variable 'a' referenced before assignment   
  原因分析,直接上dis模块解析bar的汇编代码。得到以下结果:

12           0 LOAD_FAST                0 (a)
3 LOAD_CONST               1 (1)
6 INPLACE_ADD
7 STORE_FAST               0 (a)
10 LOAD_CONST               0 (None)
13 RETURN_VALUE   
  可以看到,造成这个异常的结果是LOAD_FAST没有找到local变量。STORE_FAST语句的作用是绑定一个local变量。那么在储存变量之前就先去读,当然是会报错了。可是,明明是a = a + 1。而按照赋值语句先执行右边的规律来看,他应该先去外层的a那里读取值,然后再新建一个local的名字a,把值赋给local的a啊?
  原因暂且放下,先看一段能正常执行的代码。
  把前面代码中的a = a + 1改成b = a + 1。反汇编得到以下代码。

13           0 LOAD_DEREF               0 (a)
3 LOAD_CONST               1 (1)
6 BINARY_ADD
7 STORE_FAST               0 (b)
10 LOAD_CONST               0 (None)
13 RETURN_VALUE         
  果然按照原来设想的一样,a在这个地方变成了LOAD_DEREF,变成了访问外围的值,然后和1想加以后,储存在一个本地的变量b里面。
  正确的程序和错误的程序的差别就是,错误的里面,a是赋值语句的左边。
  这看起来不经心的一个差别,会不会是原因呢?答案是YES!看python的PEP227中的一段话。

PEP227 写道

  If a name is bound anywhere within a code block, all uses of the
    name within the block are treated as references to the current
    block.

 这句话非常拗口。我换一种通俗的方式来解释一下。模拟一下python编译器的行为。首先编译器看到了a = a + 1这句话,发现这是一个赋值语句。先检查右边,遇到了一个名字叫做a的东西。a是什么?编译器问自己。会不会是一个局部变量?于是编译器就傻傻的找到规则,规则表说:如果一个名字出现在参数声明,赋值语句(左边),函数声明,类声明,import语句,for语句和except语句中,这就是一个局部变量。ok。编译器从头到尾一看,a就在一个赋值语句的左边,那么a是一个局部变量没跑了。于是生成一条记录LOAD_FAST 0。你是局部变量,让你运行快一点。接着,分析完右边分析左边,赋值语句左边一定是一个局部变量,简单,你就在0号位置把,直接生成STORE_FAST 0,把栈顶的值给你。编译器顺利的编译结束。下面轮到虚拟机运行了。虚拟运行到这个语句就犯糊涂了,叫我LOAD_FAST 0。可是0里面什么东西都没有啊。我擦勒。只好报错了。
 
而第二段代码为什么能够正确执行呢?其实就是因为,编译器在整个代码块里面没有发现有绑定名字给a,也没有发现a是一个global对象,所以,就生成一个LOAD_DEREF  语句,告诉虚拟机,a不在这个里面。到别的地方去找他。
 
那么这个别的地方究竟是什么地方呢?如果python没有这个一定是局部变量的规则,是不是就能修改了呢?
 
我们继续分析。
 
先找到LOAD_DEREF的定义是什么?查看dis这个模块的说明,里面有如下的文字:
 
 

DIS 写道

LOAD_DEREF(i)
Loads the cell contained in slot i of the cell and free variable storage. Pushes a reference to the object the cell contains on the stack.

 
大意就是,加载cell到栈顶。cell是一个什么?这时候,联想到Python的CodeObject里面有一个属性叫做co_cellvars.会不会和这个有关?
 
查了文档以后发现如下定义:
 
 

DataModel 写道

co_cellvars is a tuple containing the names of local variables that are referenced by nested functions;

 
被嵌套的函数引用的局部变量?好奇特的说法啊。真这么神奇?执行下列代码。
 
 

def foo():
a = 1
def bar():
b = a + 1
print 'bar cellvars:', bar.func_code.co_cellvars
foo()
print 'foo cellvars:', foo.func_code.co_cellvars
 
执行结果是:
 
 

bar cellvars: ()
foo cellvars: ('a',)   
 
还真是的,a在bar中引用了,所以被加入到cellvars里面。需要注意的是,他这里只是把名字放到了cellvar中,也就是说,这个闭包中的对象,依然只是一个引用而已。当这个bar调用的时候,是会顺着引用找到真正的值的。而如果真正的值被修改,在所有的bar里面都会体现。
 
这个过程是怎么加入的呢?反汇编一下foo的代码:
 
 

  2           0 LOAD_CONST               1 (1)
3 STORE_DEREF              0 (a)
3           6 LOAD_CLOSURE             0 (a)
9 BUILD_TUPLE              1
12 LOAD_CONST               2 (<code object bar at 0x48f458, file "test.py", line 3>)
15 MAKE_CLOSURE             0
18 STORE_FAST               0 (bar)
21 LOAD_CONST               0 (None)
24 RETURN_VALUE         
 看到奇特的STORE_DEREF, LOAD_CLOSURE, MAKE_CLOSURE指令。
 
这三个指令的作用分别如下:
 
 

dis 写道

STORE_DEREF(i)¶
Stores TOS into the cell contained in slot i of the cell and free variable storage.

 
 

dis 写道

LOAD_CLOSURE(i)
Pushes a reference to the cell contained in slot i of the cell and free variable storage. The name of the variable is co_cellvars if i is less than the length of co_cellvars. Otherwise it is co_freevars[i - len(co_cellvars)].

 
 

dis 写道

MAKE_CLOSURE(argc)
Creates a new function object, sets its func_closure slot, and pushes it on the stack. TOS is the code associated with the function, TOS1 the tuple containing cells for the closure’s free variables. The function also has argc default parameters, which are found below the cells.

 
看来是编译器发现foo函数里面有一个嵌套的bar函数以后,就把在bar中引用的局部变量a放到一个cell当中,然后将所有的对象都生成成一个tuple,赋值给bar这个funcobject的func_closure。
 
为了查看神奇的效果,写下面一段代码运行一下看看:
 
 

def foo():
a = 1
def bar():
b = a + 1
return bar
b = foo()
print 'bar func_closure:', b.func_closure
 
如果这程序按照猜测的结果运行,那么将会返回一个cell的tuple。执行结果如下。
 
 

bar func_closure: (<cell at 0x454690: int object at 0x803388>,)  
 
果然不出所料。那么func_closure的作用在文档里面怎么描述呢?
 
 

datamodel 写道

func_closureNone or a tuple of cells that contain bindings for the function’s free variables.Read-only

 
看来这个东东涉及到的是Python的名字查找顺序的问题。先local,再闭包,再global。
 
详细内容可以参看PEP227里面有这么一句话。
 
 

PEP227 写道

    The implementation adds several new opcodes and two new kinds of
    names in code objects.  A variable can be either a cell variable
    or a free variable for a particular code object.  A cell variable
    is referenced by containing scopes; as a result, the function
    where it is defined must allocate separate storage for it on each
    invocation.  A free variable is referenced via a function's
    closure.

    The choice of free closures was made based on three factors.
    First, nested functions are presumed to be used infrequently,
    deeply nested (several levels of nesting) still less frequently.
    Second, lookup of names in a nested scope should be fast.
    Third, the use of nested scopes, particularly where a function
    that access an enclosing scope is returned, should not prevent
    unreferenced objects from being reclaimed by the garbage
    collector.



 
相信看到前面func_closure是readonly,大家一定非常失望。看看别的语言的实现如何。
 
javascript的版本1。
 
 

        function foo(){
var num = 1;
function bar(){
var num = num + 1;
alert(num);
}
bar()
}
foo();

 
这个版本会报NaN。。说明Python的问题Javascipt也有。
 
那如果说num不声明为var呢?
 
 

        function foo(){
var num = 1;
function bar(){
num = num + 1;
alert(num);
}
bar()
}
foo();

 
正确提示2.。
 
要是Python也有这样的机制好了。。
 
令人高兴的是,python3里面终于改观了。从语法到底层全都支持了(貌似是一个性质)。
 
语法上加上了nonlocal关键字。
 
 

def foo():
a = 1
def bar():
nonlocal a
a = a + 1
print(a)
return bar
foo()()


 
正确返回2!!
 
底层加上了可爱的下面两个函数。
 
 

PyObject* PyFunction_GetClosure(PyObject *op)¶
Return value: Borrowed reference.
Return the closure associated with the function object op. This can be NULL or a tuple of cell objects.
int PyFunction_SetClosure(PyObject *op, PyObject *closure)
Set the closure associated with the function object op. closure must be Py_None or a tuple of cell objects.
Raises SystemError and returns -1 on failure.

 
终于可以操作闭包了。哈哈哈哈。。
 
其实说到最后,如果python中有种机制能支持匿名代码块就好了。嘿嘿。到此结束。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-368162-1-1.html 上篇帖子: Python和Ruby语言对比 下篇帖子: Python Code Style
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表