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
浙公网安备 33010602011771号