装饰器(Decorators)是 Python 的一个重要部分,一般采用语法糖的形式,是一种语法格式。它可以很方便地在不改变函数原有功能的基础上扩展功能,是 python 中非常好用的一个高级功能,但是本身并不容易理解和掌握。
装饰器本质上是一个Python函数(其实就是闭包),它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。装饰器用于有以下场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。
装饰器的优点
- 解耦业务功能和非业务功能,不改变被装饰的函数的源代码的情况下扩展函数的功能。
- 使用灵活,只需要加一行代码即可,复用方便,没有侵入性。
装饰器的分类
在 python 中,装饰器主要可以分为以下几类:
- 装饰器本身是否带参数
- 函数装饰器还是类装饰器
装饰器的原理
在理解装饰器的原理前,我们先理解一下 python 中的函数调用以及闭包的概念。
函数调用
在 python 中,一切皆对象,函数本身也可以作为普通参数传递:
def a():
print("a func")
def b(func):
return func
c = b(a)
c()
在上面的例子中,我们定义了两个函数 a 和 b ,b 函数可以接受一个函数作为参数,并且通过一个 c 变量接收,c可以直接调用,等于是调用了 a 。
闭包
在函数中,我们可以再定义一个内部函数,这个内部函数对(非全局作用域)外部作用域的变量进行引用,那么这个内部函数称为闭包。闭包每次运行是能记住引用的外部作用域的变量的值,这个内部函数只能在所属的外部函数中调用:
def outer(var_1):
def inner(var_a):
print(var_a)
inner(var_1)
# 打印 2
outer(2)
# 无法在函数 outer 之外调用,会报错找不到 inner 函数
inner(2)
装饰器本质
理解了上面两点以后,就可以明白装饰器的实现原理了,装饰器的本质就是将被装饰的函数通过穿参的形式传给装饰器函数:
# 装饰器语法糖形式
@outer
def test():
pass
# 上面的 @ 等同于下面的调用方式
a = outer(test)
在 python 中,提供了很多语法糖,用来使写法更加简洁和优雅。@ 就是装饰器的语法糖,在一个定义一个函数时,上方加上 @ 就能实现一个装饰器,如上面的 @outer 。
函数装饰器
函数装饰器,顾名思义就是通过定义函数来装饰。
不带参数
当装饰器不需要参数控制的时候,一个不带参数的简单装饰器即可:
def decorator_1(func):
def wrapper(*args, **kwargs):
# do something
return func(*args, **kwargs)
# do something
return wrapper
func 表示要被装饰的函数,也就是上面被 outer 装饰的 test,当 test 本身需要传递一些参数时,我们可以在内部函数 wrapper 中通过*args, **kwargs
接收,例如:
def decorator_1(func):
def wrapper(*args, **kwargs):
print(args)
return func(*args, **kwargs)
# do something
return wrapper
@decorator_1
def test(*args):
pass
# 执行结果:(4,5,6)
test(4,5,6)
带参数
带参数的装饰器为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的,那我们就需要实现一个能传参数的装饰器函数:
def decorator_2(*args, **kwargs):
# 传给装饰器的参数
print(args)
print(kwargs)
def wrapper(func):
def inner(*args, **kwargs):
# 传给 func 被装饰函数本身的参数
print(args)
print(kwargs)
return func(*args, **kwargs)
return inner
return wrapper
# 被装饰函数
@decorator_2(100, b=5)
def test(*args, **kwargs):
pass
test(4, b=2, c=3)
# 打印结果依次为(100,) {'b': 5} (4,) {'b': 2, 'c': 3}
带参数的装饰器实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包;当给装饰器传参数时,Python 能够发现这一层的封装,并把参数传递到外层装饰器的环境中。
类装饰器
不止是函数,类也可以作为装饰器,相比于函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__魔法方法,当使用 @ 形式将类附加到函数上时,调用函数就会调用类的__call__方法。
不带参数
class decorator_3:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print(args)
print(kwargs)
self.func(*args, **kwargs)
@decorator_3
def test(*args, **kwargs):
pass
# 打印 (4,) {'b': 2, 'c': 3}
test(4, b=2, c=3)
带参数
在类装饰器中,我们可以通过__init__方法,给类装饰器传入参数,比如实现一个可以传入日记级别参数来控制不同日志打印逻辑的装饰器:
class Decorator_4:
def __init__(self, level=None):
self.level = level
def __call__(self, func):
print(self.level)
def wrapper(*args, **kwargs):
print(args)
print(kwargs)
return func(*args, **kwargs)
return wrapper
@Decorator_4(level="error")
def test(*args, **kwargs):
pass
多装饰器
当我们有多个附加功能的时候,也是可以添加多个装饰器的,比如我们有下面这样的定义:
@a
@b
@c
def d():
pass
# 上面等同于下面
f = a(b(c(d)))
wraps语法糖
我们上面的例子中,有些装饰器是通过 wrapper 这种闭包的内部函数返回的,它会导致一个问题:丢失了原来函数对象的一些属性,比如__name__,__doc__等属性,而且 __wrapped__属性指向包装函数
functools.wrap 可用于装饰器的内层函数, 抵消装饰器的副作用, 因为使用装饰器后, 原函数被内层函数赋值覆盖, 函数名称等信息丢失了(装饰器仅仅是不改变函数原有功能)。
def decorator_2(*args, **kwargs):
def wrapper(func):
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
return wrapper
def decorator_3(*args, **kwargs):
def wrapper(func):
@functools.wraps(func)
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
return wrapper
@decorator_2()
def test(*args, **kwargs):
pass
@decorator_3
def test1(*args, **kwargs):
pass
test(5, 6, a=7)
print(test.__name__) # inner
test1(5, 6, a=7)
print(test.__name__) # test