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

 

posted @ 2019-01-09 19:55  GUXH  阅读(463)  评论(0编辑  收藏  举报