Python装饰器应用--实现一个cache装饰器

  1. 简化设计,函数的形参定义不包括可变位置参数,可变关键字参数和keyword-only参数
  2. 可以不考虑缓存大小,也不用考虑存满了之后的换出问题
  3. 进阶如下
def add(x=4, y=5):
	time.sleep(3)
	return x + y

以下6种,可以认为是同一种调用
print(1, add(4,5))
print(2, add(4))
print(3, add(y=5)
print(4, add(x=4, y=5))
print(5, add(y=5, x=4))
print(6, add())

数据类型的选择

  • 缓存的应用场景,是有数据需要频繁查询,且每次查询都需要大量计算或者等待时间之后才能返回结果的情况,使用缓存来提高查询速度,用内存空间换取查询、加载的时间

cache应该选用什么数据结构?

  • 便于查询的,且能快速获得数据的数据结构
  • 每次查询的时候,只需要输入一致,就应该得到同样的结果(顺序也一致,例如减法函数,参数顺序不一致,结果不一样)
  • 基于上面的分析,此数据结构应该是字典
  • 通过一个key,对应一个value
  • key是参数列表组成的结构,value是函数返回值。难点在于key如何处理

key的存储

  • key必须是hashable
  • key能接受到位置参数和关键字参数传参
  • 位置参数是被收集在一个tuple中的,本身就有顺序
  • 关键字参数被收集在一个字典中,本身无序,这会带来一个问题,传参的顺序未必是字典中保存的顺序。
    OrderedDict,它可以记录顺序。
    不用OrderedDict,用一个tuple保存排过序的字典的item的kv对。

key要求

  • key必须是hashable
  • 由于key是所有实参组合而成,而且最好要作为key的,key一定要可以hash,但是如果key有不可hash类型数据,就无法完成,因为lru_cache就不可以
def add1(x,y):
	return y
	
>>>add1([],5)
5

@functools.lru_cache()
def add(x,y=5):
	time.sleep(3)
	return y
	
>>>add(4)
5

>>>add([],5)
------------------------------------------------------------------
TypeError					 	Traceback (most recent call last)
<ipython-input-46-a6b43c6e78a2> in <module>()
----> 1 add([],5)

TypeError: unhashable type: 'list'
  • 缓存必须使用key,但是key必须可hash,所以只能使用可hash的实参的函数调用

key算法设计

  • inspect模块获取函数签名后,取parameters,这是一个有序字典,会保存所有参数的信息。
  • 构建一个字典params_dict,按照位置顺序从args中依次对应参数名和传入的实参,组成kv对,存入params_dict中。
  • kwargs所有值update到params_dict中。
  • 如果使用了缺省值的参数,不会出现在实参params_dict中,会出现在签名的parameters中,缺省值也在函数定义中。

调用的方式

  • 普通的函数调用可以,但是过于明显,最好类似lru_cache的方式,让调用者无察觉的使用缓存。
    构建装饰器函数。
    代码模板如下:
from functools import wraps
import inspect
def py_cache(fn):
	local_cache = {} # 对不同函数名是不同的cache
	@wraps(fn)
	def wrapper(*args, **kwargs): # 接收各种参数
		# 参数处理,构建key
		ret = fn(*args, **kwargs)
		return ret
	return wrapper
@py_cache
def add(x,y,z=6):
	return x + y + z

目标

def add(x, y, z=6):
	return x + y + z
	
add(4, 5)
add(4, 5, 6)
add(4, z=6, y=5)
add(4, y=6, z=6)
add(x=4, y=5, z=6)
add(z=6, x=4, y=5)

上面几种都等价,也就是key一样,这样都可以缓存。

代码实现

  • 完成了key的生成:本次使用了普通的字典params_dict,先把位置参数的对应好,再填充关键字参数,最后补充缺省值,然后再排序生成key。
from functools import wraps
import inspect

def py_cache(fn):
    local_cache = {} # 对不同函数名是不同的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # 参数处理, 构建key
        sig = inspect.signature(fn)
        params = sig.parameters # 只读有序字典

        param_names = [key for key in params.keys()] # list(params.keys())
        param_dict = {} # 目标参数字典

        # 有序字典
        # for i, v in enumerate(args):
        #     k = param_names[i]
        #     param_dict[k] = v
        param_dict.update(zip(params.keys(), args))

        # 关键字参数
        # for k, v in kwargs.items():
        #     param_dict[k] = v
        param_dict.update(kwargs)

        # 缺省值处理
        for k in (params.keys() - param_dict.keys()):
            param_dict[k] = params[k].default

        # for k, v in params.items():
        #     if k not in param_dict.keys():
        #         param_dict[k] = v.default

        key = tuple(sorted(param_dict.items()))

        # 判断是否需要缓存
        if key not in local_cache.keys():
            local_cache[key] = fn(*args, **kwargs)

        return key, local_cache[key]
    return wrapper

import time
@py_cache
def add(x, y, z=6):
    time.sleep(2)
    return x + y +z

result = []
result.append(add(4, 5))
result.append(add(4, 5, 6))
result.append(add(4, z=6, y=5))
result.append(add(4, y=5, z=6))
result.append(add(x=4, y=5, z=6))
result.append(add(z=6, x=4, y=5))

for x in result:
    print(x)
  • 增加logger装饰器查看执行时间
from functools import wraps
import datetime

def logger(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta =(datetime.datetime.now() - start).total_seconds()
        print(fn.__name__, delta)
        return ret
    return wrapper

@logger
@py_cache
def add(x, z, y=6):
    time.sleep(3)
    return x + y + z

过期功能

  • 一般缓存系统都有过期功能。
  • 它是某一个key过期。可以对每一个key单独设置过期时间,也可以对这些key统一设定过期时间
  • 本次的实现简单点,统一设定key的过期时间,当key生存超过了这个时间,就自动被清除
  • 注意:这里并没有考虑多线程等问题。而且这种过期机制,每一次都要遍历所有数据,大量数据的时候,遍历可能有效率问题
  • 在上面的装饰器中增加一个参数,需要用到了带参装饰器了
  • @py_cache(5) 代表key生存5秒钟后过期
  • 带参装饰等于在原来的装饰器外面在嵌套一层

清除的时机

  • 何时清除过期key?
    1、用到某个key之前,先判断是否过期,如果过期重新调用函数生成新的key对应value值。
    2、一个线程负责清除过期的key.本次在创建key之前,清除所有过期的key。

  • value的设计
    1、key => (v, createtimestamp)
    适合key过期时间都是统一的设定。
    2、key => (v, createtimestamp, duration)
    duration是过期时间,这样每一个key就可以单独控制过期时间。在这种设计中,-1可以表示永不过期,0可以表示
    立即过期,正整数表示持续一段时间过期。
    本次采用第一种实现。

#简化设计,函数的形参定义不包括可变位置参数,可变关键字参数和keyword-only参数
#可以不考虑缓存大小,也不用考虑存满了之后的换出问题
from functools import wraps
import inspect
import datetime
import time

def logger(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print(fn.__name__, delta)
        return ret
    return wrapper

def py_cache(duration):
    def _cache(fn):
        local_cache = {} # 对不同函数名是不同的cache
        @wraps(fn)
        def wrapper(*args, **kwargs):
            def clear_expire(cache):
                # 使用缓存时才清楚过期的key
                expire_keys = []
                for k ,(_, stamp) in cache.items():
                    now = datetime.datetime.now().timestamp()
                    if now - stamp > duration:
                        expire_keys.append(k)
                for k in expire_keys:
                    cache.pop(k)

            clear_expire(local_cache)

            def make_key():
                # 参数处理,构建key
                sig = inspect.signature(fn)
                params = sig.parameters # 只读有序字典

                param_names = [key for key in params.keys()] # list(params.keys())
                param_dict = {} # 目标参数字典

                # 有序参数
                # for i, v in enumerate(args):
                #     k = param_names[i]
                #     param_dict[k] = v
                param_dict.update(zip(params.keys(), args))

                # 关键字参数
                # for k, v in kwargs.items():
                #     param_dict[k] = v
                param_dict.update(kwargs)

                # 缺省值处理
                for k in (params.keys() - param_dict.keys()):
                    param_dict[k] = params[k].default

                # for k, v in params.items():
                #     if k not in param_dict.keys():
                #         param_dict[k] = v.default

                return tuple(sorted(param_dict.items()))
            key = make_key()
            # 待补充, 增加判断是否需要缓存
            if key not in local_cache.keys():
                local_cache[key] = (fn(*args, **kwargs))
                datetime.datetime.now().timestamp() # 时间戳

            return key, local_cache[key]

        return wrapper
    return _cache

@logger
@py_cache(10)
def add(x, z, y=6):
    time.sleep(3)
    return x,y

result = []
result.append(add(4, 5))
result.append(add(4, 5, 6))
result.append(add(4, z=6, y=5))
result.append(add(4, y=5, z=6))
result.append(add(x=4, y=5, z=6))
result.append(add(z=6, x=4, y=5))

for x in result:
    print(x)

time.sleep(10)
result = []
result.append(add(4, 5))
result.append(add(4, z=5))
result.append(add(4, y=6, z=5))
result.append(add(4, 6))
  • 如果要使用OrderedDict,要注意,顺序要以签名声明的顺序为准
def make_key():
	# 参数处理,构建key
	sig = inspect.signature(fn)
	params = sig.parameters # 只读有序字典
	
	params_dict = OrderedDict() # {}

	params_dict.update(zip(params.keys(), args))

	# 缺省值和关键字参数处理
	# 如果在params_dict中,说明是位置参数
	# 如果不在params_dict中,如果在kwargs中,使用kwargs的值,如果也不在kwargs中,就使用缺省值
	for k,v in params.items(): # 顺序由前面的顺序定
		if k not in params_dict.keys():
			if k in kwargs.keys():
				params_dict[k] = kwargs[k]
			else:
				params_dict[k] = v.default
	return tuple(params_dict.items())			

装饰器的用途

  • 装饰器是AOP面向切面编程 Aspect Oriented Programming的思想的体现。
  • 面向对象往往需要通过继承或者组合依赖等方式调用一些功能,这些功能的代码往往可能在多个类中出现,例如
  • logger功能代码。这样造成代码的重复,增加了耦合。logger的改变影响所有使用它的类或方法。
  • 而AOP在需要的类或方法上切下,前后的切入点可以加入增强的功能。让调用者和被调用者解耦。
  • 这是一种不修改原来的业务代码,给程序动态添加功能的技术。例如logger函数就是对业务函数增加日志的功能,
  • 而业务函数中应该把与业务无关的日志功能剥离干净。

装饰器应用场景

  • 日志、监控、权限、审计、参数检查、路由等处理。
  • 这些功能与业务功能无关,是很多业务都需要的公有的功能,所以适合独立出来,需要的时候,对目标对象进行增强。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值