一文看懂 Python 的装饰器
目录
在Python中使用装饰器可以在不修改代码的前提下为已有的函数添加新功能,例如打印日志、缓存数据等。
为什么需要装饰器
假如你需要为某个函数添加一个新功能。直接的办法是在该函数中实现这个功能,或者在新的函数中实现它,然后在该函数中调用新函数。
间接的办法是执行新的函数,新函数实现了该功能,并调用原来的函数。装饰器便是这样的函数。
既然直接的办法能解决问题,为什么还需用装饰器?
在实际中,随着业务的变化,项目会越来越复杂,比如:
类之间重复的代码越来越多。
项目代码包含大量与业务无关的功能,例如调试、缓存、鉴权等。
为了解决上述问题,第一个办法是把重复的代码和业务无关的功能抽象出来,然后在新的类中实现。这样一来,新类与旧类在项目中耦合,新类的改变会影响到旧类,项目的维护成本增加。
第二个思路是,在旧类需要的时候(编译或运行时)动态地加入这些功能,即 面向切面编程。切面代表了一个功能点,站在业务的角度,它是与业务逻辑无关的功能。
面向对象的思想是把业务逻辑划分成不同的类,而面向切面为业务逻辑提供补充功能。前者是纵向切分,后者是横行切分。一纵一横,保持项目代码简洁。
装饰器实现的就是切面功能点。下文用一个例子说明三种类型点装饰器:
- 普通装饰器
- 带参数的装饰器
- 装饰器类
普通装饰器
我们即将实现一个 计时器 timer
,它的功能是计算函数的运行时间。使用效果如下所示:
# main.py
import time
@timer
def test():
time.sleep(1.0)
if __name__ == '__main__':
test()
time cost: 1.0013980865478516
装饰器 timer
的输入参数是一个函数对象,返回的结果也是一个函数对象。返回的函数是一个 附加了新功能的函数。
在上面的例子中, 装饰器 @timer
实际上是一个执行如下操作的语法糖。
wrapper_func = timer(test)
wrapper_func()
完整代码如下所示。
import time
# 装饰器函数
# 输入是一个函数, 返回一个装饰过的函数wrapper
def timer(func):
# 调用func并计时
def wrapper(*args, **kwargs):
t1 = time.time()
func(*args, **kwargs)
print("time cost:", time.time() - t1)
return wrapper
@timer
def test():
time.sleep(1.0)
if __name__ == '__main__':
test()
装饰后的函数名
在上述例子中,如果我们打印使用装饰器之后的函数 test
的名称,结果是 wrapper
。
>>> print(test.__name__)
wrapper
在一些情况下,我们希望保持原来函数的名称,这时可以利用Python自带的装饰器 @wraps(func)
来装饰 wrapper
,从而保持函数名称不变(代码如下)。
import time
from functools import wraps
# 装饰器函数
# 输入是一个函数, 返回一个装饰过的函数wrapper
def timer(func):
# 调用func并计时
@wraps(func) # 保持func的函数名
def wrapper(*args, **kwargs):
t1 = time.time()
func(*args, **kwargs)
print("time cost:", time.time() - t1)
return wrapper
带参数的装饰器
下面我们要把计时器的功能稍微扩充一下。实现一个带参数的计时器@timer_unit(unit)
,其中参数 unit
代表了计时的单位: 's' -- 秒; 'ms' -- 毫秒
。
前面提到,装饰器函数的输入必须是一个函数对象。注意到函数名加括号 timer_unit(unit='s')
代表执行一个函数,因此只要让函数 timer_unit(unit='s')
的执行结果返回一个普通的装饰器即可。
# 带参数的装饰器
def timer_unit(unit='s'):
# 普通装饰器
def decorator(func):
# 调用func并计时
@wraps(func) # 保持func的函数名
def wrapper(*args, **kwargs):
t1 = time.time()
func(*args, **kwargs)
multiplier = 1
if unit == 'ms':
multiplier = 1000
print("time cost (unit: %s):" % unit, (time.time() - t1) * multiplier)
return wrapper
# 返回普通装饰器
return decorator
装饰器类
我们还要给上述装饰器增加新的计时单位 'mus: 微秒'
。直接的方法是在 wrapper
函数中增加相应的逻辑。
这样做的缺点是,随着装饰器功越来越复杂,如果所有的逻辑在一个函数里实现,不利于协作和维护。这时可以用类的方式来实现装饰器的功能。
import time
from functools import wraps
class Timer(object):
""" 一个用来计时的装饰器类
需要实现__call__方法, 可以像函数一样调用实例
"""
def __init__(self):
self._multiplier = 1 # 秒
self._unit = None # 's'-秒; 'ms'-毫秒; 'mus' - 微秒
def _get_multiplier(self):
if self._unit == 'ms': # 毫秒
self._multiplier = 1000
if self._unit == 'mus': # 微秒
self._multiplier = 1000000
return self._multiplier
def __call__(self, unit='s'):
self._unit = unit
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
t1 = time.time()
func(*args, **kwargs)
print("time cost (unit: %s):" % unit, (time.time() - t1) * self._get_multiplier())
return wrapper
return decorator
# 装饰器函数
timer_class = Timer()
@timer_class(unit='mus')
def test():
time.sleep(1)
if __name__ == '__main__':
test()
最后说一句,不要滥用装饰器。