Python装饰器

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

装饰器

装饰器是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

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值