装饰器就是一个函数,一般用于包裹着一个函数并在这个函数前后可以调用其他方法,达到类似切面编程的效果,返回的也是函数,而且它可以在不改变函数情况下,进行增强功能。一般用于返回结果缓存和参数检验等。
先看一个场景,例如我们要统计某个函数的运行时间,一般来说都会这样处理:
import time
def f():
print("f() begin")
time.sleep(1)
print("f() end")
begts = time.time()
f()
print(time.time() - begts)
这样处理往往需要在f前后进行增加计算时间逻辑,如果考虑把f作为参数传入另一个函数,然后再调用f这样不就可以不需要在f前后修改也可以重用,例子代码如下:
import time
def f():
print("f() begin")
time.sleep(1)
print("f() end")
def run_time(call_func):
begts = time.time()
call_func()
print(time.time() - begts)
call_func()
但又冒出一个问题,调用f的人就需要更改调用的地方,有没有一种方法可以不修改函数定义的情况下,增强这个f函数的功能呢?Python的装饰器就是为此而生的。
from functools import wraps
import time
def time_stat(func):
@wraps(func)
def wrapper():
begts = time.time()
result = func()
print(time.time() - begts)
return wrapper
@time_stat
def f():
print("f() begin")
time.sleep(1)
print("f() end")
f()
# 输出
f() begin
f() end
1.00132799149
time_stat
就是一个装饰器,用@func_name
在函数名称定义上方便可以包裹了f函数。而在time_stat
的定义里面,传入func作为函数这里就是f,@wraps
是为了还原f的属性这个后面再说,wrapper
是在函数内部定义的一个函数,里面的逻辑是先进行时间记录然后调用f函数,再打印运行时间。最后time_stat
这个函数返回的就是wrapper的函数。调用的时候顺序是:
调用f->调用time_stat(f)->调用wrapper()
虽然调用的是f,实际上其实是调用wrapper了,如果在wrapper出现异常的时候,那么就不知道究竟在调用哪个方法?调用的人就会一脸茫然,我什么时候调用过wrapper?所以@wraps
就是为了尽量把f的属性赋值给wrapper,使得wrapper的调用就像调用f一样,其实就可以想象wraps
究竟做了些什么操作了,这里说到赋值属性,其实在Python的世界里面都是一个对象,像函数也是一个对象,所以它有自己的属性。
最后是一个装饰器带参数的实际例子,需要在包裹函数外层再定义一层函数,这样的话就有三层函数,虽然复杂一点为了能传参没办法了。
import json
from functools import wraps
def get_cache_key(sj, prefix, *args, **kwargs):
params = []
params.append(sj)
params.append(prefix)
if args:
args_str = "_".join(map(str, args))
params.append(args_str)
if kwargs:
keys = sorted(kwargs.keys())
kwargs_str = "_".join([str(kwargs.get(key)) for key in keys])
params.append(kwargs_str)
key = "_".join(params)
return key
def rc(prefix, timeout=10):
def dcf(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
rdb = self.get_redis()
mdb = self.get_mongodb()
sj = self.sj
# 获取force,看是否需要强制跳过缓存逻辑
force = kwargs.get("force", False)
# 如果获取redis、mongodb实例失败或者强制执行函数标识force为True,直接执行函数
if not rdb or not mdb or force:
return func(self, *args, **kwargs)
# 生成redis key
key = get_cache_key(sj, prefix, *args, **kwargs)
# 从redis获取,看看有没有缓存的结果
result = rdb.get(key)
if result:
# 有缓存结果直接返回
return json.loads(result)
# 把执行结果缓存到redis
# result = func(self, *args, **kwargs)
# rdb.setex(key, timeout, json.dumps(result))
return result
return wrapper
return dcf
class Task(object):
def __init__(self):
super(Task, self).__init__()
self.sj = "task"
def get_redis(self):
print(u"获取redis实例")
return None
def get_mongodb(self):
print(u"获取mongodb实例")
return None
# 前缀user_info,过期时间5秒
@rc("user_info", timeout=5)
def get_user_info(self, uid):
return {"uid": uid}
task =Task()
task.get_user_info(121)
获取redis实例
获取mongodb实例
{'uid': 121}
装饰器的使用很广泛,做函数参数校验、二级缓存等,用起来的确会很方便快速增强某些函数能力,像最后给出的二级缓存例子,在初期开发需求的时候并没有这么大的流量访问,那么并不需要用到,到了用户量激增的时候,发现每次都需要执行大量逻辑后才能获取到客户端想要的结果,而且这个结果在一定时间甚至是1秒内都不会有太大变化,但可能1秒内的访问量已经成千上万了,这样设置1秒的缓存都可以有效地避免90%以上的重复执行逻辑,但对于逻辑代码只是仅仅增加@rc
这个装饰器,其他的修改并没有,只要装饰器并没有问题,那么修改的带来的风险会相当小,相对比带来的性能优化是显著的。
后视镜 2019年11月25日
网友评论