zoukankan      html  css  js  c++  java
  • guxh的python笔记三:装饰器

    1,函数作用域

    这种情况可以顺利执行:

    total = 0
    def run():
        print(total) 
    

    这种情况会报错:

    total = 0
    def run():
        print(total)
        total = 1
    

    这种情况也会报错:

    total = 0
    def run():
        total += 1   # 等效total = total + 1
    

    原因是函数内部对total有定义后,解释器会认为total是局部变量,但是内部执行时,却发现total还没定义。

    解决办法是将total声明为全局变量:

    total = 0
    def run():
        global total
        ......
    

      

    2,自由变量和闭包

    自由变量可以用来保持额外的状态。

    什么时候需要保存额外的状态呢?

    比如需要对未知输入做不断累加,需要有地方专门存放过去的累加值,然后将新增输入不断累加进去。

    类似还有移动平均数的计算,需要有专门的地方存放累加值和累加的次数。

    由于普通函数内部的变量在运行一次后,就会消失,无法保存额外状态,因此就需要借助其他手段。

    2.1,当保存的额外状态是不可变对象时(数字,字符,元组)

    方法一,全局变量

    total = 0   # 额外状态
    def run(val):
        global total
        total += val
        print(total)
    
    run(1)  # 1
    run(2)  # 3
    run(3)  # 6
    

    使用全局变量不具备可扩展性:

    1)如果想更改初始total值得找到全局变量total再修改,无法通过传参形式设定total

    2)代码没法重用,不能给别人调用。

    方法二,闭包

    用高阶函数,把total封装在里面(先别管为什么这叫闭包,后面会做定义和总结)

    def cal_total():
        total = 0   # 额外状态
        def run(val):
            nonlocal total
            total += val
            print(total)
        return run
    
    run = cal_total()
    run(1)  # 1
    run(2)  # 3
    run(3)  # 6
    

    稍作改变,还可以允许用户传参设定total的初始值(默认为0):

    def cal_total(total=0):
        def run(val):
            nonlocal total
            total += val
            print(total)
        return run
    
    run = cal_total(10)
    run(1)  # 11
    run(2)  # 13
    run(3)  # 16
    

      

    方法三,类

    单个方法的类,用类的属性来保存额外状态:

    class Total:
        def __init__(self, total=0):
            self.total = total   # 额外状态
        def run(self, val):
            self.total += val
            print(self.total)
    
    t = Total(10)
    t.run(1)  # 11
    t.run(2)  # 13
    t.run(3)  # 16

    为什么会有单个方法的类?因为要保留额外的状态给该方法,比如本例中的total,需要保留下来。

    单个方法的类可以用闭包改写。

    除了通过对象的方法去调用对象保存的额外状态,还可以通过协程,和functools.partial / lambda去调用,详见函数-高阶函数笔记。

    2.2,保存额外状态是可变对象时(字典,列表)

    方法一:全局变量

    total = []
    def run(val):
        total.append(val)
        print(sum(total))
    
    run(1)  # 1
    run(2)  # 3
    run(3)  # 6
    

      

    方法二,闭包

    def cal_total(total=None):
        total = [] if total is None else total
        def run(val):
            total.append(val)
            print(sum(total))
        return run
    
    run = cal_total([10])
    run(1)  # 11
    run(2)  # 13
    run(3)  # 16
    

      

    方法三,类

    class Total:
        def __init__(self, total=None):
            self.total = [] if total is None else total
        def run(self, val):
            self.total.append(val)
            print(sum(self.total))
    
    t = Total([10])
    t.run(1)  # 11
    t.run(2)  # 13
    t.run(3)  # 16

    方法一和方法二中,并没有对total声明global / nonlocal,因为total是容器类型,对其修改时并不会创建副本,而是会就地修改,但如果在函数内部对total有赋值时,就会变成函数内部变量:

    total = []
    def run():
        total = [1, 2, 3]
      
    run()
    print(total)  # [], 此时全局total和局部total没关系
    

    甚至可能会报错:

    total = []
    def run(val):
        total.append(val)   # UnboundLocalError: local variable 'total' referenced before assignment
        total = [1, 2, 3]
    

    如果想在内部修改外部定义的total,同样需要声明global(全局) / nonlocal(闭包):

    total = []
    def run():
        global total
        total = [1, 2, 3]
    
    run()
    print(total)  # [1, 2, 3]
    

    2.3,不可变对象和可变对象的保留额外状态的方法总结

    状态是不可变对象时(数字,字符,元组),保留额外状态的方法:

    全局变量:需声明global

    闭包:需声明nonlocal

    类:无

    状态是可变对象时(字典,列表),保留额外状态的方法:

    全局变量:无需声明global

    闭包:无需声明nonlocal,需要注意防御可变参数

    类:需要注意防御可变参数

    2.4,什么是自由变量和闭包

    方法二闭包中的额外状态,即自由变量!自由变量 + run函数即闭包!

    可以对自由变量和闭包做个简单总结:

    自由变量定义:1)在函数中引用,但不在函数中定义。2)非全局变量。

    闭包定义:使用了自由变量的函数 + 自由变量

    如果把闭包所在的函数看做类的话,那么自由变量就相当于类变量,闭包就相当于类变量 + 使用了类变量的类方法

    回顾2.2中的方法一和方法二:

    方法一的total不是自由变量,因为total虽然满足了“在run函数中引用,但不在run函数中定义”,但它是全局变量。

    方法二的total即自由变量。因为total满足:1)run函数中引用,但不在run函数中定义。2)total不是全局变量。

    方法二的total + run函数组成了闭包。 

    2.5,闭包的自由变量到底存在哪?

    函数也类,因此闭包本质上也可以看做是建了个类,然后把额外状态(自由变量)当作类的实例属性来存放的,那么它到底存在哪呢?

    还是这个例子:

    def cal_total(total=None):
        total = [] if total is None else total
        def run(val):
            total.append(val)
            print(sum(total))
        return run
    
    run = cal_total([10])
    run(1)  
    run(2)

    可以把run看成是cal_total类的实例,试试能不能访问total:

    print(run.total)  # AttributeError: 'function' object has no attribute 'total'  

    只能继续查查看其他属性,发现有一个叫‘__closure__’的属性:

    print(type(run))   # <class 'function'>
    print(dir(run))    # [..., '__class__', '__closure__', '__code__', ...]
    

    进一步打印,发现__closure__是个长度为1的元组,说明它可以存放多个闭包的自由变量:

    print(run.__closure__)        #(<cell at 0x00000148E02794F8: list object at 0x00000148E03D65C8>,)
    print(type(run.__closure__))  # <class 'tuple'>
    print(len(run.__closure__))   # 1

    这个唯一的元素是个cell类,并且有个cell_contents属性:

    print(type(run.__closure__[0]))  # <class 'cell'>
    print(dir(run.__closure__[0]))   # [..., 'cell_contents']

    尝试打印该属性,正是辛苦找寻的自由变量:

    print(run.__closure__[0].cell_contents)  # [10, 1, 2]
    run.__closure__[0].cell_contents = [1, 2, 3]  # AttributeError: attribute 'cell_contents' of 'cell' objects is not writable
    

    访问起来比较麻烦!并且无法进行赋值。如果想访问闭包自由变量,可以编写存取函数:

    def cal_total(total=None):
        total = [] if total is None else total
    
        def run(val):
            total.append(val)
            print(sum(total))
    
        def get_total():
            return total
    
        def set_total(components):
            nonlocal total
            total = components
    
        run.get_total = get_total  # get_total是cal_total下的函数,需要把它挂到run下面,一切皆对象,给run动态赋上方法,类似猴子补丁
        run.set_total = set_total  
        return run
    
    run = cal_total([10])
    run(1)   # 11
    run(2)   # 13
    print(run.get_total())    # [10, 1, 2]
    run.set_total([1, 1, 1])  # [1, 1, 1]
    print(run.get_total())
    run(1)   # 4
    

    3,基本装饰器

    3.1,单层装饰器

    单层装饰器:

    import time
    def timethis(func):
        st = time.time()
        func()
        print(time.time() - st)
        return func
    
    @timethis   # 等效于run = timethis(run)
    def run():
        time.sleep(2)
        print('hello world')  # 执行了两遍
        return 1  # 返回值无法被调用方获取
    
    ret = run()
    print(ret)  # None

    存在问题:

    被装饰函数中的功能会被执行两遍(func执行一遍后再返回func地址,调用方获取地址后再次执行)

    无法返回(并且被装饰函数有返回值时无法获取到)

    无法传参(被装饰函数有参数时无法传参)

    3.2,双层装饰器 — 标准装饰器

    2次传参:外层传函数,内层传参数。

    2次返回:第一次返回被装饰函数运行后的结果,第二次返回内层装饰器地址

    def cal_time(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            st = time.time()
            result = func(*args, **kwargs)
            print(time.time() - st)
            return result
        return wrapper
    
    @cal_time  # 等效于run = cal_time(run)
    def run(s):
        time.sleep(2)
        return '{:-^21}'.format(s)  # 执行居中打印import time
    
    >>>run('hello world')
    2.0003201961517334
    -----hello world-----
    

    第二次return wrapper,相当于@cal_time装饰run函数时,run = cal_time(run),让run拿到内层wrapper函数地址,运行run('hello world')时,实际运行的是wrapper('hello world')。

    第一次return result,相当于让result拿到run函数运行后的结果。

    如果想访问原始函数,可以用__wrapped__:

    >>>run.__wrapped__('hello world')
    -----hello world-----
    

    3.3,三层装饰器 — 可接受参数的装饰器

    假如想让装饰器能够接收参数,当传入'center'时,输出的时间能够精确到小数点后一位并且居中打印,可以使用三层装饰器:

    def cal_time(ptype=None):
        def decorate(func):
            fmt = '{:-^21.1f}' if ptype == 'center' else '{}'
            @wraps(func)
            def wrapper(*args, **kwargs):
                st = time.time()
                result = func(*args, **kwargs)
                print(fmt.format(time.time() - st))
                return result
            return wrapper
        return decorate
    
    @cal_time('center')
    def run(s):
        time.sleep(2)
        return '{:-^21}'.format(s)
    
    >>>run('hello world')
    ---------2.0---------
    -----hello world-----
    

    不传入参数时:

    @cal_time()
    def run(s):
        time.sleep(2)
        return '{:-^21}'.format(s)
    
    >>>run('hello world')
    2.0021121501922607
    -----hello world-----
    

    备注:

    如果想实现不传入参数时用法与双层装饰器保持一致(@cal_time),同时又可以接受参数,即可选参数的装饰器,详见4.1

    如果想实现可接受参数,并且可以更改属性的装饰器,详见4.2

    4,高级装饰器 

    4.1,双层装饰器 - 可选参数的装饰器

    标准的可选参数装饰器,通过3层装饰器实现,依次传入:参数/被装饰函数地址/被装饰函数参数

    本例用双层就能搞定可选参数,是因为直接在外层传入参数和被装饰函数地址,然后通过partial绑定了参数

    def cal_time(func=None, *, ptype=None):
        if func is None:
            return partial(cal_time, ptype=ptype)
        fmt = '{:-^21.1f}' if ptype == 'center' else '{}'
        @wraps(func)
        def wrapper(*args, **kwargs):
            st = time.time()
            result = func(*args, **kwargs)
            print(fmt.format(time.time() - st))
            return result
        return wrapper
    
    @cal_time(ptype='center') # 装饰时,必须用关键字参数,否则传入字符会被装饰器当作func
    def run(s):
        time.sleep(2)
        return '{:-^21}'.format(s)
    
    >>>run('hello world')
    ---------2.0---------
    -----hello world-----
    

    不传入参数时,与标准的双层装饰器一致:

    @cal_time
    def run(s):
        time.sleep(2)
        return '{:-^21}'.format(s)
    
    >>>run('hello world')
    2.001026153564453
    -----hello world-----
    

    4.2,三层装饰器 — 可接受参数,并且可以更改属性的装饰器

    装饰时可以添加参数,装饰完以后,可以更改属性的装饰器

    def attach_wrapper(obj, func=None):
        if func is None:
            return partial(attach_wrapper, obj)
        setattr(obj, func.__name__, func)
        return func
    
    def cal_time(ptype=None):
        def decorate(func):
            fmt = '{:-^21.1f}' if ptype == 'center' else '{}'
            @wraps(func)
            def wrapper(*args, **kwargs):
                st = time.time()
                result = func(*args, **kwargs)
                print(fmt.format(time.time() - st))
                return result
    
            @attach_wrapper(wrapper)
            def set_fmt(new_fmt):
                nonlocal fmt
                fmt = new_fmt
    
            return wrapper
        return decorate
    
    @cal_time('center')
    def run(s):
        time.sleep(2)
        return '{:-^21}'.format(s)
    
    >>>run('hello world')
    ---------2.0---------
    -----hello world-----
    >>>run.set_fmt('{:->21.1f}')   # 直接更改装饰器的fmt属性
    >>>run('hello world')
    ------------------2.0
    -----hello world-----
    

    4.3,能够实现函数参数检查的装饰器

    对函数的参数进行检查可以通过:property,工厂函数,描述符

    本例演示了通过装饰器对函数参数的检查:

    def typeassert(*ty_args, **ty_kwargs):
        def decorate(func):
            if not __debug__:  # -O或-OO的优化模式执行时直接返回func
                return func
    
            sig = signature(func)
            bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments
    
            @wraps(func)
            def wrapper(*args, **kwargs):
                bound_values = sig.bind(*args, **kwargs)
                for name, value in bound_values.arguments.items():
                    if name in bound_types:
                        if not isinstance(value, bound_types[name]):
                            raise TypeError('Argument {} must be {}'.format(name, bound_types[name]))
                return func(*args, **kwargs)
            return wrapper
        return decorate
    
    @typeassert(int, int)
    def add(x, y):
        return x + y
    
    >>>add(1, '3')
    TypeError: Argument y must be <class 'int'>
    

    4.4,在类中定义装饰器

    property就是一个拥有getter(), setter(), deleter()方法的类,这些方法都是装饰器

    为什么要这样定义?因为多个装饰器方法都在操纵property实例的状态

    class A:
        def decorate1(self, func):  # 通过实例调用的装饰器
            @wraps(func)
            def wrapper(*args, **kwargs):
                print('Decorate 1')
                return func(*args, **kwargs)
            return wrapper
    
        @classmethod
        def decorate2(cls, func):  # 通过类调用的装饰器
            @wraps(func)
            def wrapper(*args, **kwargs):
                print('Decorate 2')
                return func(*args, **kwargs)
            return wrapper
    
    a = A()
    
    @a.decorate1   # 通过实例调用装饰器
    def spam():
        pass
    
    @A.decorate2   # 通过类调用装饰器
    def grok():
        pass
    
    >>>spam()
    Decorate 1
    >>>grok()
    Decorate 2
    

    4.5,把装饰器定义成类 

    python一切皆对象,函数也是对象,可以从类的角度看待装饰器。

    装饰函数的2步操作,第一步可以看作是cal_time类的实例化,第二步可以看做是cal_time类实例的__call__调用:

    run = cal_time(run)   # @cal_time
    run('hello world')    # 调用cal_time类的实例run

    通过类改写二层装饰器:

    class cal_time:
        def __init__(self, fun):
            self.fun = fun
    
        def cal_timed(self, *args, **kwargs):
            st = time.time()
            ret = self.fun(*args, **kwargs)
            print(time.time() - st)
            return ret
    
        def __call__(self, *args, **kwargs):
            return self.cal_timed(*args, **kwargs)
    
    @cal_time
    def run(s):
        time.sleep(2)
        return '{:-^21}'.format(s)  # 执行居中打印
    
    result = run('hello world')
    print(result)  
    
    output:
    2.0132076740264893
    -----hello world-----  

    从上可以看到,装饰器可以通过函数实现,也可以通过类实现。

    这与闭包中保存额外状态的实现方法类似,保存额外状态可以通过类实例的属性(方法三),也可以通过闭包的自由变量(方法二)。

    当然闭包中“方法三”没有实现__call__,因此需要通过“实例.方法”的方式去调用,也可以参照装饰器类的实现做如下改造:

    class cal_total:
    
        def __init__(self, total=None):
            self.total = [] if total is None else total
    
        def run(self, val):
            self.total.append(val)
            print(sum(self.total))
    
        def __call__(self, val):
            return self.run(val)
    
    
    run = cal_total([10])
    run(1)  # 11
    run(2)  # 13
    run(3)  # 16

    同样,本例中的装饰器实现也可以实现闭包“方法三”的效果,即通过“实例.方法”的方式去调用。

    因此,本质上完全可以把嵌套函数(闭包,装饰器),看做是类的特例,即实现了可调用特殊方法__call__的类。

    再回头看看,闭包在执行run = cal_total(10)时,装饰器在执行@cal_time,即run = cal_time(run)时,都相当于在实例化,前者在实例化时输入的是属性,后者实例化时输入的是方法。

    然后,闭包执行run(1),装饰器执行run('hello world')时,都相当于调用实例__call__方法。

    python cookbook要求把装饰器定义成类必须实现__call__和__get__:

    class Profiled:
        def __init__(self, func):
            wraps(func)(self)
            self.ncalls = 0
    
        def __call__(self, *args, **kwargs):
            self.ncalls += 1
            return self.__wrapped__(*args, **kwargs)
    
        def __get__(self, instance, cls):
            if instance is None:
                return self
            else:
                return types.MethodType(self, instance)
    
    @Profiled
    def add(x, y):
        return x + y
    
    >>>add(2, 3)
    5
    >>>add(4, 5)
    9
    >>>add.ncalls
    2
    

    4.6,实现可以添加参数的装饰器

    def optional_debug(func):
        @wraps(func)
        def wrapper(*args, debug=False, **kwargs):
            if debug:
                print('Calling', func.__name__)
            return func(*args, **kwargs)
        return wrapper
    
    @optional_debug
    def spam(a, b, c):
        print(a, b, c)
    
    >>>spam(1, 2, 3)
    1 2 3
    >>>spam(1, 2, 3, debug=True)
    Calling spam
    1 2 3
    

    4.7,通过装饰器为类的方法打补丁

    常用打补丁方法有:mixin技术,元类(复杂),继承(需要理清继承关系),装饰器(速度快,简单) 

    def log_getattribute(cls):
        orig_getattribute = cls.__getattribute__
        def new_getattribute(self, name):
            print('Getting x:', name)
            return orig_getattribute(self, name)
        cls.__getattribute__ = new_getattribute
        return cls
    
    @log_getattribute
    class Foo:
        def __init__(self, x):
            self.x = x
    
    >>>f = Foo(5)
    >>>f.x
    Getting x: x
    5
    

    5,其他

    装饰器作用到类和静态方法上:需要放在@classmethod和@staticmethod下面

    所有代码中涉及到到库主要包括:

    from inspect import signature
    from functools import wraps, partial
    import logging
    import time
    
  • 相关阅读:
    返回顶部
    C# 对文本文件的几种读写方法
    cocos2dx 锁定30帧设置
    AndroidManifest.xml 屏幕上下反转
    粒子系统主
    CCParticleSystem粒子系统
    精灵的优化
    cocos2dx 菜单按钮回调方法传参 tag传参
    cocos2dx跨平台使用自定义字体
    ios7 Cocos2dx 隐藏状态栏设置
  • 原文地址:https://www.cnblogs.com/guxh/p/10246551.html
Copyright © 2011-2022 走看看