`
lin_llx
  • 浏览: 127315 次
  • 性别: 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 的实现方式不知道是怎样的, 不过看这样子用起来够呛的.
我觉得也许允许内部变量覆盖外部变量本身就是个大错误...

相关推荐

    二元关系的闭包运算

    二元关系的闭包运算则是研究这些关系性质的重要工具。本主题将深入探讨二元关系闭包运算的理论、计算方法以及如何通过编程实现可视化。 首先,我们需要理解什么是二元关系。在集合A上,一个二元关系R是A的子集,...

    Python 研究(Dive Into Python) 5.4

    6. **函数式编程**:Python支持函数式编程概念,如高阶函数、闭包、装饰器和生成器。这些工具使代码更简洁、可读性更强。 7. **迭代器和生成器**:Python的迭代器协议允许对象以迭代方式访问。生成器是轻量级的迭代...

    SmallPython

    `function`和`module`对象的内部结构在SmallPython中有详细呈现,包括函数的闭包、装饰器的实现以及模块的导入机制。 五、异常处理与错误报告 Python的异常处理机制使得程序能够优雅地处理错误。在SmallPython中,...

    python从入门到精通笔记_python教程_python_

    Python是一种高级编程语言,以其简洁明了的语法和...配合实际的编程练习,你将能够运用Python解决各种实际问题,无论是在学术研究、数据分析还是软件开发领域。所以,准备好你的编辑器,让我们一起探索Python的世界吧!

    python-handbook_handbook_python_ebook_

    通过《Python Handbook》这本电子书,读者将全面地学习Python编程,无论是在学术研究、数据科学,还是Web开发等领域,都能找到实用的指导和参考资料。同时,不断实践和应用所学知识,将有助于成为一位熟练的Python...

    简明Python教程(Python3中文版)

    **Python简介** Python是一种高级编程语言,以其...通过系统学习并实践,你将能够熟练掌握Python,为从事机器学习、深度学习等领域的研究打下坚实基础。记得理论结合实践,不断探索和尝试,才能更好地提升编程技能。

    Python-2.0.1 C语言版源码

    8. **函数式编程元素**:Python支持高阶函数、闭包和列表推导等函数式编程特性。C语言源码将揭示这些特性的实现细节。 9. **线程和并发**:Python-2.0.1的源码还包含了对多线程的支持,尽管Python的全局解释器锁...

    Python程序设计习题答案.rar

    7. **函数式编程**:可能涉及高阶函数、闭包、装饰器等概念,展示Python在函数式编程风格上的特点。 8. **正则表达式**:教授如何使用Python的re模块进行文本匹配和搜索,进行数据清洗和预处理。 9. **网络编程**...

    python源码剖析

    这一过程涉及到了许多Python的核心概念,如作用域规则、闭包、生成器、装饰器等,这些都会通过源码解析的方式进行深入探讨。 此外,Python的类型系统是另一个重要的研究方向。Python是动态类型语言,这意味着变量在...

    面向项目的_Python程序设计_教学实践与研究

    6. **函数式编程**:探讨Python中的高阶函数,如map、filter和reduce,以及lambda表达式和闭包等概念。 7. **Web开发**:如果面向项目,可能会涉及到Django或Flask等Python Web框架的基础知识,如路由、模板渲染、...

    python核心源码-chm文件

    通过深入阅读和研究"py_s.chm",开发者不仅可以掌握Python的基本语法,还能深入理解Python的运行机制,这对于成为一名资深Python开发者至关重要。同时,这个资源也可能包含一些调试技巧、扩展开发以及对Python源码的...

    Python3程序设计2版SourceCode.zip

    8. **高级特性**:可能涵盖生成器、上下文管理器、装饰器、闭包等Python3的高级特性,通过实例帮助读者理解这些复杂但强大的工具。 9. **单元测试**:源代码可能包含单元测试的编写,介绍如何使用unittest或pytest...

    Python源码剖析

    5. **作用域和命名空间**:Python的局部作用域、全局作用域以及闭包是如何工作的?源码中`PyObject *locals, *globals`的使用解释了这些概念。 6. **异常处理**:Python的异常处理基于try/except/finally结构,源码...

    Python-剑指Offer原书是用C实现的这里本人用Python对其中大部分使用了Python实现

    6. **函数式编程**:Python支持函数式编程风格,如高阶函数、闭包、生成器等,可能会在某些题目中体现。 7. **文件操作**:读写文件、处理日志等实际操作,考察对Python I/O的理解。 8. **网络编程**:Python提供...

    Python源码剖析-深度探索动态语言核心技术(2008)

    7. **生成器与协程**:深入研究Python的生成器(generator)和协程(coroutine),它们在异步编程中的应用,以及如何通过yield关键字实现控制流。 8. **类与继承**:详细剖析Python的面向对象特性,如类的定义、...

    python-2.7.14

    通过研究这些官方文档,开发者不仅可以学习Python 2.7的基本语法和高级特性,还可以深入了解如何编写高效、可维护的Python代码,同时为向Python 3的迁移做好准备。因此,对于任何想在Python 2.7环境下工作的开发者来...

    SICP(python中文带书签)

    总之,《计算机程序的构造与解释》(SICP)是一本值得深入研究的著作,它通过Lisp和Python等语言,向我们展示了计算世界的广阔与深邃。无论你是初学者还是经验丰富的程序员,这本书都能为你提供新的视角和思考方式。...

    Python编程金典

    作为Python学习资料,它适合初学者入门,也适合有经验的开发者深入研究。 在Python的世界里,语法简洁明了,易于上手,但其强大的功能和广泛的应用领域使其成为现代编程语言中的翘楚。Python支持多种编程范式,包括...

Global site tag (gtag.js) - Google Analytics