在python代码中我们经常看到函数上方放了一个@开头的函数像是带了一顶帽子一般,在python语法中称这种写法叫装饰器。对装饰器比较好的解释是:基于高阶函数和闭包在不改变原先函数或者类调用方式的前提下增加新的功能的装饰函数或者类就是装饰器。
今天将介绍以下几种装饰器基本涵盖我们会用到装饰器的所有用法,一文带你深入了解装饰器(值得收藏)。
- 普通装饰器
- 带参装饰器
- 类装饰器
- 带参类装饰器
- partial偏函数实现的类装饰器
- 装饰类的装饰器
- 异步任务装饰器的实现task.delay()
装饰器常用的场景有:
- 缓存
- 参数检查
- 记录日志
- 计算运行时间
- 单例模式
- 异常捕捉
- 数据库事物
装饰器的用法还有很多,这里我们将以参数检查的例子来展开各种写法的装饰器。
一、普通装饰器(入门用法)
普通装饰器: 写装饰器最基础的用法,要好好理解这个@语法糖是怎样展开的
def decorator(func):
def wrap(*args, **kwargs):
for price in args:
if not isinstance(price, int):
raise Exception("参数不是数字类型")
return func(*args, **kwargs)
return wrap
@decorator # 这个@语法糖在刚加载时等价于 @decorator = decorator(funn)= decorator(amount_price)
def amount_price(price_1, price_2):
print(price_1 + price_2)
return price_1 + price_2
amount_price(1, 2) # 没有改变函数的调用方式, 在调用函数的时候装饰器的作用等价于 decorator(amount_price)(1, 2)
amount_price(1, '2')
运行结果如下:
可以看到我们运行结果,当我们传入的参数全是数字类型时,得到运行结果3, 当我们传入了一个字符串‘2’时,我们的装饰函数就抛出异常,达到我们说的装饰器的作用 没有改变函数的调用方式 给函数增加了新的功能。
二、带参装饰器(进阶用法)
带参装饰器; 是在普通装饰器的基础上加多一层,可以供外部给装饰器内传递参数。
def decorator(a):
def function(func):
def wrap(*args, **kwargs):
for price in args:
if price == a:
raise Exception(f"传入的参数不能是{a}")
if not isinstance(price, int):
raise Exception("参数不是数字类型")
return func(*args, **kwargs)
return wrap
return function
@decorator(0) # 当程序加载时等价于 @decorator(0)=decorator(0)(func)
def amount_price(price_1, price_2):
print(price_1 + price_2)
return price_1 + price_2
amount_price(1, 2)
amount_price(1, 0) # 当程序运行时等价于 decorator(0)(amount_price)(1, 0)
amount_price(1, '2')
运行结果:
带参装饰器就是给装饰器一个入口,让我们可以给装饰器内部传递参数达到我们的一些目的,比方缓存装饰器时,我们会给装饰器传递缓存过期时间决定缓存多久过期等。在上面的例子中,我们传入了一个0的参数,表示被装饰函数的参数中不能出现0。
三、类装饰器(进阶用法)
类装饰器:顾名思义是用类作为我们的装饰器,其中我们可以利用类的__init__方法和__call__方法(ps:对象object加()会触发__call__方法,这个知识要提前储备)来实现它,我们看示例。
class Decorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
for price in args:
if not isinstance(price, int):
raise Exception("参数不是数字类型")
return self.func(*args, **kwargs)
@Decorator # 程序加载时等价于 @Decorator=Decorator(func)
def amount_price(price_1, price_2):
print(price_1 + price_2)
return price_1 + price_2
amount_price(1, 2) # 程序运行时等价于 Decorator(amount_price)(1, 2)
amount_price(1, '2')
运行结果如下:
这就是用类装饰器去实现了普通装饰器的功能。
四、带参数的类装饰器(进阶用法)
带参数的类装饰器: 在类装饰的基础上,再利用类的__call__方法加闭包的方式实现,其中关键在于怎么去利用__init__和__call__方法去分配我们@语法糖展开的参数。
class Decorator:
def __init__(self, limit):
self.limit = limit
def __call__(self, func):
def wrap(*args, **kwargs):
for price in args:
if price == self.limit:
raise Exception(f"传入的参数不能是{self.limit}")
if not isinstance(price, int):
raise Exception("参数不是数字类型")
return func(*args, **kwargs)
return wrap
@Decorator(0) # 程序加载时 @Decorator(0) = Decorator(0)(func)
def amount_price(price_1, price_2):
print(price_1 + price_2)
return price_1 + price_2
amount_price(1, 2)
amount_price(1, 0) # 程序运行时 Decorator(0)(func)(1, 0)
运行结果如下:
看到这里的时候是不是有点装饰器的感觉了,其实真正容易让人犯迷糊的是这个@语法糖的展开规则,只要脑子中形成它的展开意识,装饰器的玩法就变得洒洒水了。
五、partial偏函数结合类装饰器(高级用法)
from functools import partial
class Decorator:
def __init__(self, limit, func):
self.limit = limit
self.func = func
def __call__(self, *args, **kwargs):
for price in args:
if price == self.limit:
raise Exception(f"传入的参数不能是{self.limit}")
if not isinstance(price, int):
raise Exception("参数不是数字类型")
return self.func(*args, **kwargs)
def limit(value):
return partial(Decorator, value) # 这是python标准库中的偏函数,相当于提前传入了一个或者多个默认的参数
@limit(0) # 程序加载时等价于 @limit(0)=limit(0)(func) = Decorator(0, func)
def amount_price(price_1, price_2):
print(price_1 + price_2)
return price_1 + price_2
amount_price(1, 2)
amount_price(1, 0) # 程序运行时 Decorator(0, amount_price)(1, 0)
程序运行结果如下:
其实偏函数与类装饰器的结果和带参类装饰器的效果是一样的,只是写法形式不一样而已,万变不离其宗理解@语法糖的展开规则和偏函数的用法,即可得心应手。
六、装饰类的装饰器(高级用法)
装饰类的装饰器: 之前我们的装饰器都是装饰函数,装饰类装饰器顾名思义是用来装饰类的装饰器。
def decorator(cls):
def wrap(*args, **kwargs):
for price in args:
if not isinstance(price, int):
raise Exception("参数不是数字类型")
return cls(*args, **kwargs)
return wrap
@decorator
class AmountPrice:
def __init__(self, price_1, price_2):
self.price_1 = price_1
self.price_2 = price_2
def total(self):
print(self.price_1 + self.price_2)
return self.price_1 + self.price_2
AmountPrice(1, 2).total()
AmountPrice(1, '2').total()
结果如下:
同一个应用场景,这么多种用法写到这自己都觉得有点吐,其实装饰类的装饰器这里有个常用的应用场景就是,设计模式中的单例设计模式,,有兴趣的朋友可以自己尝试写一下。
七、异步任务装饰器的实现task.delay() (高级用法)
在我使用rq或者celecy去执行异步任务时,表达是 task.delay(params) 通常task这个任务是用装饰器修饰,在我们执行表达式的时候就把task任务push进我们的消费队列中。 这时如果我们有一个需求是,我们要给所有这些异步任务加入一个记录日志的功能,我们不可能直接去修改异步框架的源码,所以有我们接下来这个异步任务装饰器的实现。
但是为了保证整齐划一,我们还是以参数检查的场景来写这个装饰器。
from functools import wraps
class RQ:
def __call__(self, func):
@wraps(func)
def delay(*args):
func(*args)
for price in args:
if not isinstance(price, int):
raise Exception("参数不是数字类型")
func.delay = delay
return func
class MineRQ(RQ):
def __init__(self, limit):
self.limit = limit
super().__init__()
def __call__(self, func):
@wraps(func)
def limit(*args):
for price in args:
if price == self.limit:
raise Exception(f"传入的参数不能是{self.limit}")
func(*args)
return super().__call__(limit)
@MineRQ(0)
def amount_price(price_1, price_2):
print(price_1 + price_2)
return price_1 + price_2
amount_price.delay(2, 1)
amount_price.delay(2, 0)
运行结果如下:
这个RQ类装饰器是异步任务模块自带的装饰器,MineRQ为我们自己实现的装饰器,这样完美的实现了我们额外添加新功能的额外要求
要素总结:
- 掌握@语法糖的展开规则
- 掌握高阶函数和闭包的用法
- 掌握类的__init__和__call__方法
- 掌握偏函数的用法