装饰器
装饰器是AOP面向切面编程Aspect Oriented Programming的思想的体现.
它是一种不改变原来的业务代码,给程序动态添加功能的技术.
无参装饰器
□ 无参装饰器
□ 首先,装饰器是一个高阶函数
□ 装饰器要有一个函数作为参数
□ 装饰器的返回值也是一个函数
□ 可以使用@functionname方式简化调用
□ 它是对传入的函数的功能增强,对原有函数不做改变
假设现在我们有一个简单的需求,在add(x, y)函数调用前后分别打印一句话,并且不能对add函数本身进行变动.
传统写法
def add(x, y):
return x + y
def logger(fn, *args, **kwargs):
print("before ....")
ret = fn(*args, **kwargs)
print(ret)
print("after ....")
#调用:
logger(add, 1, 5)
#还可以进一步柯里化
def logger(fn):
def wrapper(*args, **kwargs):
print("before ....")
ret = fn(*args, **kwargs)
print(ret)
print("after ....")
return ret
return wrapper
#调用:
logger(add)(1, 5)
# 解析:
# logger(add)返回值为wrapper,它是一个函数
# 而wrapper(1, 5)即为调用
上面的调用可以写成:
add = logger(add)
add(1, 5)
# 这里第一步add = logger(add) 可能比较容易产生歧义
#
# 下面我们从最开始定义add函数时开始进行分析
# add是一个标识符,它引用了一个函数fnc(fnc内存引用计数+1), 而logger(add),这里由于logger函数内的
# wrapper函数也用到了fnc,所以它的内存引用计数+1(假设此时为2),然后再来看add = logger(add)
# 这一行,等号左边,由重新定义了add这个标识符,此时,它对原函数不再引用,
# 因此原函数的引用计数-1(即2-1=1),但此时fnc依然有被引用,所以此函数还是存在的.
# 而logger的返回值是wrapper函数,这时add被赋值为wrapper的引用,而不再是fnc的引用,
# 此时wrapper的内存引用计数+1
# 那么此时的add已经不是最开始的add了,它其实是对wrapper的引用
装饰器写法
接下来我们就引出了装饰器语法
def logger(fn):
def wrapper(*args, **kwargs):
print("before ....")
ret = fn(*args, **kwargs)
print(ret)
print("after ....")
return ret
return wrapper
@logger
def add(x, y):
return x + y
# 这里的@logger等价于 add = logger(add)
# 调用
add(1, 5)
这里之所以叫做无参装饰器,是因为@logger并没有被手动写入任何参数,@符号+函数名logger
解析:
我们为add方法添加了装饰器@logger,此时相当于调用logger函数,并且将被添加装饰器标记的函数(这里为add)当作参数传递给装饰器函数(logger),最后将logger函数的返回值赋值给被添加装饰器标记的函数(add函数)
它等价于add = logger(add)
带参装饰器
□ 带参装饰器与无参装饰器的区别
□ 返回值是一个不带参的装饰器函数
□ 可以看作是在装饰器外面又加了一层函数
注:值得注意的是,用上面的方法添加无参装饰器后,调用add.__name__,ad..__doc__等等的属性值会变为装饰器函数(wrapper)的属性.因此,为了解决这个问题我们可以使用functools模块

我们可以使用functools中的wraps方法,来进行装饰,写法很简单@wraps即可

上面的@functools.wraps(fn)中,fn是要被装饰的函数,这个是最常用的带参装饰器
举例
假设我们现在又有一个需求,调用add方法时可以传入一个boolean值,False时正常显示,True时在计算结果后添加-号,即输出负数

相当于在原有的装饰器基础上又加了一层函数,这个函数接受@logger(True)传入的参数,并且这个函数名称一定要与装饰器标记处一致,也就是说,原来的logger函数名要给新加的外层函数使用,而原来的logger函数要改名(这里改为了_logger)
实现一个cache装饰器
functools模块中有一个lru_cache方法,可以用来进行缓存,换句话说,用空间换取时间,但是这个方法针对关键字参数(kwargs)或带默认值的参数支持的不是特别好,以下几种调用被视为不同的参数调用:
①add(4, 5)
②add(4, y=5)
③add(x=4, y=5)
但是add(y = 5, x= 4)和③被视为相同.下面我们来自己写一个cache装饰器,让上述情况都走缓存.
第一步:首先写出框架,随后逐步完善:
import functools
import time
# 装饰器函数add_cache
def add_cache(fn):
# 用来存放结果的字典对象(key:结果)
local_cache = {}
@functools.wraps(fn)
def wrapper(*args, **kwargs):
# 定义一个字典,用来存放key
key_dict = {}
# 基于*args,对key进行参数结构化
for v in args:
pass
# 基于**kwargs,对key进行参数结构化
for k, v in kwargs.items():
pass
# 未传参,基于默认值
# todo
ret = fn(*args, **kwargs)
return ret
return wrapper
# 目标函数add
@add_cache
def add(x=4, y=5):
time.sleep(5)
return x + y
第二步:做key,要求key一定是hashable的
import functools
import time
import inspect
# 装饰器函数add_cache
def add_cache(fn):
# 用来存放结果的字典对象(key:结果)
local_cache = {}
@functools.wraps(fn)
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
# 调用inspect模块的signature方法,取得方法的信息
sig = inspect.signature(fn)
# sig.parameters的返回值是一个有序字典OrderedDict
param_dict = sig.parameters
# 定义一个字典,用来存放key
key_dict = {}
# 基于*args,对key进行参数结构化
param_keys = [k for k in param_dict.keys()]
for i, v in enumerate(args):
key = param_keys[i]
key_dict[key] = v
# 基于**kwargs,对key进行参数结构化
key_dict.update(kwargs)
# 未传参,基于默认值
for k, v in param_dict.items():
if k not in key_dict:
# v是param object 它的default属性可返回参数默认值
key_dict[k] = v.default
# dict无序,所以要用sorted函数进行排序
# key必须是不可变类型(hashable)所以这里用tuple
key = tuple(sorted(key_dict))
if key not in local_cache.keys():
ret = fn(*args, **kwargs)
local_cache[key] = ret
delta = (datetime.datetime.now() - start).total_seconds()
return local_cache[key], delta
return wrapper
# 目标函数add
@add_cache
def add(x=4, y=5):
time.sleep(5)
return x + y
测试执行结果:发现除了第一次用时超过5秒(因为有sleep),后面都没有再sleep,也就是说并没有直接调用add函数,而是从缓存中取得结果

第三步:添加过期清除功能
import functools
import time
import inspect
# 装饰器函数add_cache
def add_cache(duration):
def _add_cache(fn):
# 用来存放结果的字典对象(key:结果)
local_cache = {}
@functools.wraps(fn)
def wrapper(*args, **kwargs):
expire_keys = []
for k, (_, stamp) in local_cache.items():
now = datetime.datetime.now().timestamp()
if now - stamp > duration:
expire_keys.append(k)
for k in expire_keys:
local_cache.pop(k)
start = datetime.datetime.now()
# 调用inspect模块的signature方法,取得方法的信息
sig = inspect.signature(fn)
# sig.parameters的返回值是一个有序字典OrderedDict
param_dict = sig.parameters
# 定义一个字典,用来存放key
key_dict = {}
# 基于*args,对key进行参数结构化
param_keys = [k for k in param_dict.keys()]
for i, v in enumerate(args):
key = param_keys[i]
key_dict[key] = v
# 基于**kwargs,对key进行参数结构化
key_dict.update(kwargs)
# 未传参,基于默认值
for k, v in param_dict.items():
if k not in key_dict:
# v是param object 它的default属性可返回参数默认值
key_dict[k] = v.default
# dict无序,所以要用sorted函数进行排序
# key必须是不可变类型(hashable)所以这里用tuple
key = tuple(sorted(key_dict))
if key not in local_cache.keys():
ret = fn(*args, **kwargs)
local_cache[key] = (ret, datetime.datetime.now().timestamp())
delta = (datetime.datetime.now() - start).total_seconds()
return key, local_cache[key], delta
return wrapper
return _add_cache
# 目标函数add
@add_cache(duration)
def add(x=4, y=5):
time.sleep(5)
return x + y

本文介绍了Python装饰器的概念和用途,通过实例展示了无参装饰器的传统与装饰器语法的写法,接着讨论了带参装饰器的实现,并利用functools.wraps增强装饰器的功能。此外,还介绍了一个实现缓存功能的自定义cache装饰器,通过key管理确保缓存效果,并添加了过期清除功能。
1217

被折叠的 条评论
为什么被折叠?



