`
lin_llx
  • 浏览: 125383 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Python闭包研究

阅读更多

其实很早以前就想写这么一篇文章了。一直没有机会。正好今天和同事讨论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[i]到栈顶。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[i] 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_closure None 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中有种机制能支持匿名代码块就好了。嘿嘿。到此结束。

 

 

 

4
1
分享到:
评论
6 楼 NeuronR 2010-10-31  
lin_llx 写道
NeuronR 写道
话说回来, python 的赋值和声明语句都不带区分的, 这点很搓.
python 3 的实现方式不知道是怎样的, 不过看这样子用起来够呛的.
我觉得也许允许内部变量覆盖外部变量本身就是个大错误...


这难道不允许内部变量和外部重名?这个难度有点大了把。

这个可以有, 符号表略做点手脚就行了.
5 楼 yangjuven 2010-10-21  
doylecnn 写道
NeuronR 写道
话说回来, python 的赋值和声明语句都不带区分的, 这点很搓.
python 3 的实现方式不知道是怎样的, 不过看这样子用起来够呛的.
我觉得也许允许内部变量覆盖外部变量本身就是个大错误...


那个语言不是内部作用域覆盖外部作用域的啊...


我觉得根源是python不能区分赋值语句和声明语句,如果能区分这点,python就能很好区分内部变量和外部变量,就不存在所谓的“覆盖”问题吧
4 楼 doylecnn 2010-10-21  
NeuronR 写道
话说回来, python 的赋值和声明语句都不带区分的, 这点很搓.
python 3 的实现方式不知道是怎样的, 不过看这样子用起来够呛的.
我觉得也许允许内部变量覆盖外部变量本身就是个大错误...


那个语言不是内部作用域覆盖外部作用域的啊...
3 楼 roverll 2010-10-21  
想回个贴,还要做小测试。。。写的很好懂,虽然我没有搞过python,都能明白。
2 楼 lin_llx 2010-10-20  
NeuronR 写道
话说回来, python 的赋值和声明语句都不带区分的, 这点很搓.
python 3 的实现方式不知道是怎样的, 不过看这样子用起来够呛的.
我觉得也许允许内部变量覆盖外部变量本身就是个大错误...


这难道不允许内部变量和外部重名?这个难度有点大了把。
1 楼 NeuronR 2010-10-20  
话说回来, python 的赋值和声明语句都不带区分的, 这点很搓.
python 3 的实现方式不知道是怎样的, 不过看这样子用起来够呛的.
我觉得也许允许内部变量覆盖外部变量本身就是个大错误...

相关推荐

    谷歌COLAB中的Python概念化

    第6章和第7章深入介绍了高级Python编程概念,如迭代器、闭包、装饰器、生成器。对异常处理的良好和深入的了解,使我们能够编写出可靠和健壮的代码。为了满足这一需要,第8章介绍了Python中异常处理的突出特点。第9章...

    JAX是Autograd和XLA的结合,用于高性能机器学习研究。-Python开发

    Python + NumPy程序的可组合转换:区分,矢量化,JIT到GPU / TPU,以及更多JAX:Autograd和XLA 转型| 安装指南| 神经网络库| 变更记录| 参考文档| ...它可以通过循环,分支,递归和闭包来区分,并且可以用d

    jax:Python + NumPy程序的可组合转换:区分,向量化,JIT到GPUTPU等

    它可以通过循环,分支,递归和闭包来区分,并且可以采用派生类的派生类。 它支持通过反向模式区分(aka反向传播)以及正向模式区分,并且两者可以任意组合为任意顺序。 新功能是JAX使用在GPU和TPU上编译和运行您的...

    matlab把代码导入-closed-form-matting:A.LevinD.Lischinski和Y.Weiss的Python实现。自然

    计算机视觉和模式识别(CVPR)的研究,2006年6月,纽约 该存储库还包含在Levin,Anat,Dani Lischinski和Yair Weiss中提出的背景/前景重建方法的实现。 “自然图像抠像的封闭形式解决方案。” IEEE Transactions on ...

    JAX:支持GPU/TPU的NumPy包(Autograd/XLA),面向高性能机器学习-python

    JAX 是 Autograd 和 XLA,结合起来用于高性能机器学习研究。 借助 Autograd 的更新版本,JAX 可以自动区分原生 Python 和 NumPy 函数。 它可以通过循环、分支、递归和闭包进行微分,并且可以取导数的导数的导数。...

    论文研究-一种新的关联特征和模糊聚类的进化树构建方法.pdf

    对TBC特征矩阵进行平移极差变换,利用指数切比雪夫距离法构建了模糊相似矩阵,采用模糊聚类中的传递闭包法构建进化树。该方法不需要多序列比对,计算简单。对两组基因组序列构建进化树,实验结果验证了该方法的有效...

    程序员面试刷题的书哪个好-awesome-links:很棒的链接

    是一个序列建模工具包,允许研究人员和开发人员为翻译、摘要、语言建模和其他文本生成任务训练自定义模型。 ) [ - 预测知识图中概念之间的链接的开源 Python 库。) [(PBG)是一个分布式系统,用于学习大型图的图...

    主要的

    我们研究了双字母组语言的后缀替换闭包。 2021年2月12日星期五 我们讨论了双语法例和语言。 我们讨论了精确度和冗余度的数学符号。 对于星期一,请阅读第4单元的最高2.3节(最高64页) 我们将在星期一讨论SL2...

    微软开源 JavaScript 引擎 ChakraCore-易语言

    支持,据我当前的研究,不同线程必须有各自的runtime对象,每个runtime可以有多个环境(context),同一个runtime下的多个环境可以自由交换数据,但环境之间不共享数据。也就是说api级别可以把环境1的数据带到环境2...

Global site tag (gtag.js) - Google Analytics