前言
装饰器作为Python的基础,无论在面试还是在日常工作中用到的频率算是非常频繁的,而其本身的特点也非常适合用来考核水平,可以问的很浅,也能问的很深。在之前的学习中,我经常费劲心思去记录各种装饰器,比如普通装饰器、带参装饰器、类装饰器、带参类装饰器,觉得花里胡哨的,而平常用的不就是普通装饰器就够用了么,并且用的频率相对其他来说也不算很频繁,往往定义一个就够用上许久。
在这篇文章里,我会通俗易懂地讲述这些装饰器的存在意义,以及如何快速地记录和使用他们,话不多说,开始吧。
定义
老生常谈的定义还是绕不开的,学过设计模式的都知道23中基本设计模式里有一种就叫做装饰器模式,而Python就天然支持这样的语法来很好地装饰函数、类,所以,装饰器的作用就是在不改动原有的函数、类上,增加额外的操作处理(比如日志、计时等)。
在日常开发中用的是非常多,比如:
- 数据库事务处理:一个函数里无论调用了多少的SQL语句,在上面添加一个装饰器即可让整个函数的SQL语句都能在事务里生效;
- 登录权限验证:后台接口经常需要增加权限验证,而繁杂的权限验证又不涉及接口逻辑,这块额外的功能就非常适合装饰器
- 添加缓存:同数据库事务处理,当我们需要添加缓存这种不涉及接口业务逻辑的功能,用装饰器就ok
- …
装饰器就是用的很广泛,而它的使用也非常简单,只要定义个装饰器函数(英文decorator),给目标函数上方添加@decorator即可,一个普通的给函数计时装饰器如下:
import time
def timer(func):
def wrapper():
start = time.time()
result = func()
end = time.time()
print(func.__name__, "函数花费了(s):", end - start)
return result
return wrapper
@timer
def count():
"""this is a count"""
total = 0
for i in range(10000):
total += i
return total
count()
通过给count函数增加@timer装饰器,提供了计时这个函数整体运行的时间。除此之外,我们可以给其他任意的函数也增加该装饰器。输出如下:
count 函数花费了(s): 0.0005476474761962891
进阶
装饰器的@看着挺帅,实际上就是一种语法糖,属于Python特有,这就相当于如果你发明了另外一种语言Python2,你可以把@改为&、¥都行。所以我们要深究@究竟做了啥。
简单来说,就是将函数作为参数传给装饰器函数,上面的timer也可以这样调用,输出的结果是一样的。
timer(count)()
在上面count就是函数,将其作为参数传入timer即可,当我们把参数count传入后实际上就是调用里面的子函数:wrapper(count),所以我们可以简化timer到普通函数,但也实现装饰的效果,如下:
def timer_origin(func):
start = time.time()
result = func()
end = time.time()
print(func.__name__, "函数花费了(s):", end - start)
return result
timer_origin(count)
所以装饰器的本质就是一个传函数参的函数罢了,从这一层理解,就会发现装饰器很简单,而其他难的装饰器都是针对各式各样的需求而逐步衍生出来的。比如第一个装饰器,就是为了满足@的语法而产生的。
那基于简单的装饰器第一个问题是,如何让被调用的函数也能使用上其自身的参数呢?我们知道本质上是调用wrapper(),那给这里加上可变参数即可:wrapper(*args, **kwargs),改写如下:
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, "函数花费了(s):", end - start)
return result
return wrapper
这样无论被调用的函数是带着什么参数,都能被可变参数覆盖到,自然就满足了。那由此引发了第二个问题,我还想让装饰器也带上参数要怎么处理?不要觉得这个场景很少,恰恰相反,使用场景很频繁,比如权限验证(要给接口指定某某某的权限),比如添加缓存(要提供timeout参数来标识缓存时间等)。
那为了解决给装饰器带上参数这个问题,我们可以引入最外层函数来包裹装饰器函数,实现如下:
def timer_rule(name):
def timer(func):
def wrapper(*args, **kwargs):
if name != 'developer':
return func(*args, **kwargs)
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, "函数花费了(s):", end - start)
return result
return wrapper
return timer
@timer_rule('admin')
def count():
"""this is a count"""
total = 0
for i in range(10000):
total += i
print("total is ", total)
return total
count()
通过上面的三层函数实现了一个简单的带参装饰器,timer_rule里的参数也可以是keyword,通过这个函数,我们就可以在装饰的时候控制我们需要的装饰效果,比如只有管理员可以访问该接口,那就指定参数为’admin’即可。
接着是第三个问题,通过上面我们知道调用count函数,本质上是调用wrapper(count),那这会导致count的元数据丢失,如何处理?Python里天然有库可以解决这个问题,首先说下什么是元数据,如果这时我们打印count的下列数据:
print(count.name, count.doc)
结果是:wrapper None {}
会发现原本属于count的名字和解释都消失,变成了wrapper,这个在我们debug时会带来干扰,同时函数相关有用的信息都丢失了,那此时只要加个第三方库functools里的wraps装饰器即可,改造后如下:
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, "函数花费了(s):", end - start)
return result
return wrapper
此时再打印,结果如下:
print(count.name, count.doc)
结果是:count this is a count
总结
此次就说到这,除了函数装饰器外,当我们想用到类装饰器来更好的管理装饰器功能时,就需要用到类装饰器和带参类装饰器了,而这些留着下节再讲。