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