• python的闭包与装饰器


    原文发表在我的博客主页,转载请注明出处

    前言

    如果把python当作脚本语言,每次就是写个几十行上百行来处理数据的话,装饰器也许不是很必要,但是如果要开发一个大型系统,装饰器是躲不开的,最开始体会ryu的装饰器之美是在阅读ryu源码的时候,用python官网的一句话来说,learning about descriptors creates a deeper understanding of how python works and an appreciation for the elegance of its design。本篇文章从闭包讲起,也从闭包结束,因为如果理解了闭包,也就理解了装饰器。

    python函数作用域LEGB

    不论在什么语言中,变量都是必不可少的,在python中,一般存在以下几个变量类型,local:函数内部作用域;enclosing:函数内部与内嵌函数之间;global:全局作用域;build-in:内置作用域。解释器在遇到变量的时候,会按照如下的顺序进行查找:L > E > G > B,简称LEGB。下面以一个简单的函数来说明上述过程:

    #coding:utf-8
    #global var
    passline = 60
    def func(val):
        # for func, it's local var
        # for in_func, it's enclosing var
        passline = 90
        if val >= passline:
            print "pass"
        else:
            print "fail"
        def in_func():
            print val
        return in_func
    
    def Max(val1, val2):
        # max is a built-in fun
        return max(val1, val2)
    
    f = func(89)
    f()
    print Max(90, 100)
    

    local,global,build-in这三个类型比较容易理解,enclosing变量呢?下一节详细讲解。

    闭包理解与使用

    首先理解下python中的函数,在python中,函数是一个对象(可以通过type函数查看),在内存中占用空间;函数执行完成之后内部的变量会被解释器回收,但是如果某变量被返回,则不会回收,因为引用计数器的值不为0;既然函数也是一个对象,他也拥有自己的属性;对于python函数来说,返回的不一定是变量,也可以是函数。
    上一节提到了enclosing变量,在func函数中又定义了一个函数in_func,它输出了变量val,在输出过程中查找变量的时候发现本地没有,所以他就会去func函数里面找并且找到了,这个变量就是enclosing变量。在上面一段代码中,敏感的人可能已经发现了问题,分析代码执行的过程,func函数返回了in_func函数给了f,但是没有返回变量,所以在调用func完成之后val变量应该已经被解释器回收,但是在执行了f函数之后却仍然输出了val的值89,为什么呢?其原因就是:如果引用了enclosing作用域变量的话,会将变量添加到函数属性中,当再次查找变量时,不是去代码中查找,而是去函数属性中查找。可以通过如下代码进行验证:

    #coding:utf-8
    passline = 60
    def func(val):
        print "%x" %id(val)
        if val >= passline:
            print "pass"
        else:
            print "fail"
        def in_func():
            print val
        in_func()
        return in_func
    
    
    f = func(89)
    f() #in_func
    print f.__closure__
    

    执行上面的代码,可以发现f函数的__closure__属性拥有一个变量,这个变量的ID和func函数中val变量的ID一样。
    上面的代码是用来判断学生的成绩是否及格,即在百分制中60分及格,如果现在需要添加新的功能,即150分制中90分作为及格线,如何完成代码呢?最简单的就是我们创建两个函数,分别为func_100和func_150来完成判断,判断逻辑完全一样,代码如下:

    #coding:utf-8
    
    def func_150(val):
        passline = 90
        if val >= passline:
            print "pass"
        else:
            print "fail"
    
    def func_100(val):
        passline = 60
        if val >= passline:
            print "pass"
        else:
            print "fail"
    
    func_100(89)
    func_150(89)
    

    如果再增加应用场景呢?这样重复而没有任何技术含量的代码的增添十分繁琐,我们可以使用新的方法——闭包来解决这个问题。什么是闭包呢?闭包就是内部函数中对enclosing作用域的变量进行引用。代码如下:

    #coding:utf-8
    def set_passline(passline):
        def cmp(val):
            if val >= passline:
                print "pass"
            else:
                print "fail"
        return cmp
    
    f_100 = set_passline(60)
    f_150 = set_passline(90)
    print type(f_100)
    print f_100.__closure__
    f_100(89)
    f_150(89)
    

    在上述代码中,我们定义了一个set_passline函数,这个函数返回cmp函数,在这个函数中定义了cmp函数,在cmp函数中是我们之前的逻辑。在之后不论要进行多少分制的及格判断,只需要调用set_passline函数设置及格线就好,我们以百分制60分及格为例,分析上述代码的执行过程,基本分为两步,一是set_passline函数把返回值cmp函数给f_100,同时将60作为属性给cmp函数,二是f_100函数的执行其实相当于执行存储了passline的cmp函数。
    接下来考虑一个问题,上面的passline是整数,能否换成函数?既然变量和函数都是对象,而且以python的灵活性,答案是肯定的。下面的代码同时描述了闭包的另一个应用场景,同时展示了如何使用。

    #coding:utf-8
    def my_sum(*arg):
        if len(arg) == 0:
            return 0
        for val in arg:
            if not isinstance(val, int):
                return 0
        return sum(arg)
    
    def my_average(*arg):
        if len(arg) == 0:
            return 0
        for val in arg:
            if not isinstance(val, int):
                return 0
        return sum(arg)/len(arg)
    
    def dec(func):
        def in_dec(*arg):
            if len(arg) == 0:
                return 0
            for val in arg:
                if not isinstance(val, int):
                    return 0
            return func(*arg)
        return in_dec
    
    # 1.dec return in_dec -> my_sum
    # 2.my_sum = in_dec(*arg)
    my_sum = dec(my_sum)
    

    在上面的代码中,首先定义了两个函数,这两个函数的功能大同小异,分别为求和函数和求平均值函数,由于求平均值元祖的长度不能为空,同时元祖中的数据都应该为整数,所以在每个函数中都先需要参数检查,本着以人为本的方针,这部门代码应该复用。所以在下面定义了另外一个函数dec,这个函数的参数为一个函数,返回值为一个内嵌函数,这个内嵌函数的逻辑和上面求和求平均值的逻辑一样,先判断,再返回结果。这个函数如何使用呢?比如现在我们要求和,代码如下:

    def my_sum(*arg):
        return sum(arg)
    
    def dec(func):
        def in_dec(*arg):
            if len(arg) == 0:
                return 0
            for val in arg:
                if not isinstance(val, int):
                    return 0
            return func(*arg)
        return in_dec
    
    # 1.dec return in_dec -> my_sum
    # 2.my_sum = in_dec(*arg)
    my_sum = dec(my_sum)
    

    my_sum = dec(my_sum)函数分两步,首先my_sum得到dec返回的in_dec函数,其次调用in_dec函数。
    所以,遵上来看,闭包可以在很大程度上实现封装和代码复用。

    装饰器

    最开始就说,这篇博客始于闭包,终于闭包,所以装饰器不多说,只说四句话:
    1.装饰器就是对闭包的使用;
    2.装饰器用来装饰函数;
    3.返回一个函数对象,被装饰的函数接收;
    4.被装饰函数标识符指向返回的函数对象。

    总结

    接触python装饰器很久了,殊不知从闭包更容易理解装饰器。

  • 相关阅读:
    Node的Buffer
    node中定时器的“先进”用法
    比较setImmediate(func),setTimeout(func),process.nextTick(func)
    node.js事件触发
    node.js express的安装过程
    ”靠谱的C#“单例模式
    JS性能消耗在哪里?
    如何建立索引
    优化之sitemap+RSS
    优化のzencart URL &zenid=.....
  • 原文地址:https://www.cnblogs.com/cotyb/p/5243252.html
Copyright © 2020-2023  润新知