美文网首页
Python 内包和装饰器

Python 内包和装饰器

作者: 大雄good | 来源:发表于2018-02-18 19:26 被阅读0次

    内包和装饰器

    PythonJavaScript(暂时只研究过JS内包😆所以拿来举例)一样都支持内包,而内包又是装饰器的基础,所以本篇文章就简单总结一下本人关于内包装饰器的理解和使用。

    1.内包

    在统计或者金融领域,可能需要一个函数实现,输入一个数据,来更新当前的平均值,例如下面的avg(此例来自fluent python):

    >>> avg(10)
    10.0
    >>> avg(11)
    10.5
    >>> avg(12)
    11.0
    

    如果没有内包,我们可能会定义一个如下class来完成该需求:

    class Avg():
        def __init__(self):
            self.num_list = list()
        def __call__(self, num):
            self.num_list.append(num)
            total = sum(self.num_list)
            return total/len(self.num_list)
    

    测试效果如下:

    >>> from avg import Avg
    >>> avg1 = Avg()
    >>> avg1(1)
    1.0
    >>> avg1(9)
    5.0
    

    而使用内包之后,我们可以通过如下代码完成该功能:

    def average():
        num_list = list()
        def cal_avg(num):
            num_list.append(num)
            total = sum(num_list)
            return total/len(num_list)
        return cal_avg
    

    测试效果:

    >>> from avg import average
    >>> avg1 = average()
    >>> avg1 = average()
    >>> avg1(1)
    1.0
    >>> avg1(2)
    1.5
    

    从上面这个例子我们可以看到, 内包实际上是利用python中一切皆对象(包括函数)的特性, 通过average返回了cal_avg函数对象,而cal_avg函数对象还包括其函数域的局部变量信息。那么有疑问了,num_list并不在cal_avg的函数域(少年你骗我吧😢), 那是因为num_listcal_avg的自由变量(free variable),指不在本定义域中绑定的变量,可以查看__code__来查看编译后的变量类型:

    >>> avg1.__code__.co_varnames
    ('num', 'total')
    >>> avg1.__code__.co_f
    avg1.__code__.co_filename     avg1.__code__.co_firstlineno  avg1.__code__.co_flags        avg1.__code__.co_freevars
    >>> avg1.__code__.co_freevars
    ('num_list',)
    

    num_list的结果都保存在__closure__:

    >>> avg1.__closure__[0]
    <cell at 0x10e81f3d8: list object at 0x10ea53cc8>
    >>> avg1.__closure__[0].cell_contents
    [1, 2]
    

    下面贴出avg1的字节代码:

    >>> from dis import dis
    >>> dis(avg1)
     12           0 LOAD_DEREF               0 (num_list)
                  2 LOAD_ATTR                0 (append)
                  4 LOAD_FAST                0 (num)
                  6 CALL_FUNCTION            1
                  8 POP_TOP
    
     13          10 LOAD_GLOBAL              1 (sum)
                 12 LOAD_DEREF               0 (num_list)
                 14 CALL_FUNCTION            1
                 16 STORE_FAST               1 (total)
    
     14          18 LOAD_FAST                1 (total)
                 20 LOAD_GLOBAL              2 (len)
                 22 LOAD_DEREF               0 (num_list)
                 24 CALL_FUNCTION            1
                 26 BINARY_TRUE_DIVIDE
                 28 RETURN_VALUE
    

    从反汇编结果可以看出,avg1首先执行对num_list的解引用,然后执行append吧啦吧啦,我觉得自己表达能力有限(其实是懒),不过代码比较简单,也容易理解吧。

    nonlocal

    OK,本来我觉得这个求平均值的函数效率太低,想稍微修改一下:

    def adv_avg():
        cnt = 0
        avg = 0
        def cal_avg(num):
            cnt = cnt + 1
            avg = avg + (num - avg)/cnt 
            return avg
        return cal_avg
    

    执行一下咯,尼玛又是一个坑啊:

    >>> from avg import adv_avg
    >>> avg = adv_avg()
    >>> avg(1)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/xiowang/Documents/Py/decorator/avg.py", line 21, in cal_avg
        cnt = cnt + 1
    UnboundLocalError: local variable 'cnt' referenced before assignment
    

    为什么会这样呢?原因就是数字,字符串,元组这些不可变类型执行cnt = cnt + 1时,会创建一个新的cnt,而不是去引用。因此这时就需要用到nonlocal来显示声明变量是外部定义的:

    def adv_avg():
        cnt = 0
        avg = 0
        def cal_avg(num):
            nonlocal cnt, avg
            cnt = cnt + 1
            avg = avg + (num - avg)/cnt 
            return avg
        return cal_avg
    

    OK, 修改之后,再运行,perfect:

    >>> from avg import adv_avg
    >>> avg3 = adv_avg()
    >>> avg3(1)
    1.0
    >>> avg3(2)
    1.5
    >>> avg3(3)
    2.0
    

    2.装饰器

    对于装饰器本人理解并不深,但是从名字理解,装饰器就是给用来点缀函数或者类(都是对象)。我打个比方,古代同样两幅画,经过皇家印章盖个印,那么他添加了皇室属性,那收藏价值必须飙升呀,怎么都值个4,5线城市的小房子啊(咋最近老喜欢说房子,还不敢说上海的房子,泪啊😢),其实装饰器就是干盖章这回事呀,完成比如给函数添加打log,计时等功能。

    一个关于装饰很经典的例子,给函数添加计时功能:

    from time import time, sleep
    def decoTime(func):
        def retFunc():
            start = time()
            func()
            now = time()
            print("spend time:{:.2f}".format(now-start))
            print("func_name: {}".format(func.__name__))
        return retFunc
    
    @decoTime
    def myfunc():
        sleep(2)
    
    myfunc()
    print("myfunc name:{}".format(myfunc.__name__))
    

    执行结果如下:

    spend time:2.00
    func_name: myfunc
    myfunc name:retFunc
    

    这里我们通过上面提到的内包完成一个简单的装饰器,这里执行:

    @decoTime
    def myfunc():
    

    等价于:

    decoTime(myfunc)
    

    所以会执行retFunc函数的内容。

    functools.wraps

    另外值得注意的是,这里因为我们返回了一个retFunct函数对象,因此myfunc__name__属性也被修改为了retFunc。可是我们想保留__name__属性,那么就可以使用functools.wraps():

    from time import time, sleep
    from functools import wraps
    def decoTime(func):
        @wraps(func)
        def retFunc():
            start = time()
            func()
            now = time()
            print("spend time:{:.2f}".format(now-start))
            print("func_name: {}".format(func.__name__))
        return retFunc
    
    @decoTime
    def myfunc():
        sleep(2)
    
    myfunc()
    print("myfunc name:{}".format(myfunc.__name__))
    

    使用wraps之后的执行结果:

    spend time:2.00
    func_name: myfunc
    myfunc name:myfunc
    

    带参数的装饰器

    装饰器也是能带参数滴:

    from time import time, sleep
    from functools import wraps
    def decoTime(flag):
        if flag=="normal":
            def _decoTime(func):
                @wraps(func)
                def retFunc(*args, **kw):
                    start = time()
                    func(*args, **kw)
                    now = time()
                    print("spend time:{:.2f}".format(now-start))
                    print("func_name: {}".format(func.__name__))
                return retFunc
            return _decoTime
        elif flag=="double":
            def _decoTime(func):
                @wraps(func)
                def retFunc(*args, **kw):
                    start = time()
                    func(*args, **kw)
                    func(*args, **kw)
                    now = time()
                    print("spend time:{:.2f}".format(now-start))
                    print("func_name: {}".format(func.__name__))
                return retFunc
            return _decoTime
    
    @decoTime("normal")
    def myfunc1():
        sleep(2)
    
    @decoTime("double")
    def myfunc2():
        sleep(2)
    
    myfunc1()
    myfunc2()
    

    执行效果:

    spend time:2.00
    func_name: myfunc1
    spend time:4.01
    func_name: myfunc2
    

    说实话,这种带参数的装饰器相当ugly,三层嵌套。

    singledispatch

    singledispatch是用来实现单分派的装饰器,一个比较实用的装饰器,用来实现类似C++中的重载功能或者说解决函数中if-else过多的问题,简单通过下面的code可以说明:

    from functools import singledispatch
    @singledispatch
    def fun(arg, verbose=False):
        print(arg)
    @fun.register(int)
    def _(arg, verbose=False):
        print(type(arg))
        print(arg)
    @fun.register(list)
    def _(arg, verbose=False):
        print(type(arg))
        for i, elem in enumerate(arg):
            print(i, elem)
    
    
    fun(123)
    fun(['a', 'b', 'c'])
    

    执行结果:

    <class 'int'>
    123
    <class 'list'>
    0 a
    1 b
    2 c
    

    这里分别为fun注册了两个匿名函数,根据入参的类型,执行不同的函数。

    lru_cathe

    装饰器lru_cathe是使用lru缓存算法(底层采用key-value保存结果)为函数结果保存结果,这样重复调用函数时,会使用之前缓存的结果,而不会重复计算:

    from functools import lru_cache
    cnt1 = 0
    def fib1(n):
        global cnt1
        cnt1 = cnt1 + 1
        if n < 2:
            return n
        return fib1(n-1) + fib1(n-2)
    
    cnt2 = 0
    @lru_cache(maxsize=None)
    def fib2(n):
        global cnt2
        cnt2 = cnt2 + 1
        if n < 2:
            return n
        return fib2(n-1) + fib2(n-2)
    
    fib1(5)
    fib2(5)
    print("cnt1 : %d" %cnt1)
    print("cnt2 : %d" %cnt2)
    

    执行结果:

    cnt1 : 15
    cnt2 : 6
    

    其中maxsize是用来指定lru的最大缓存大小,默认是128,如果设置为None缓存大小为无穷大。当然官方API建议,当maxsize大小为2的指数时效率最高。

    小结

    对于python,经常面试的时候都会问到装饰器相关的内容,其实也有大牛建议使用__call__来代替装饰器的使用,其实工具的使用在于场景,但是脑袋需要知道有这些工具才能有机会比较,不然...google呀!

    相关文章

      网友评论

          本文标题:Python 内包和装饰器

          本文链接:https://www.haomeiwen.com/subject/adpbtftx.html