美文网首页Python回忆录
基础篇: 13. Python装饰器decorator

基础篇: 13. Python装饰器decorator

作者: 后视镜 | 来源:发表于2019-12-01 09:49 被阅读0次

装饰器就是一个函数,一般用于包裹着一个函数并在这个函数前后可以调用其他方法,达到类似切面编程的效果,返回的也是函数,而且它可以在不改变函数情况下,进行增强功能。一般用于返回结果缓存和参数检验等。

先看一个场景,例如我们要统计某个函数的运行时间,一般来说都会这样处理:

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日

相关文章

网友评论

    本文标题:基础篇: 13. Python装饰器decorator

    本文链接:https://www.haomeiwen.com/subject/cwczwctx.html