Python装饰器
装饰器前缘
Python装饰器对于Python初学者可能是一个比较抽象的概念,在了解装饰器之前,我们必须先熟悉Pythond方法的一个特性:Python中的方法可以像普通变量一样当做参数传递给另外一个方法,我们来看一个例子:
1 | def add(x,y): |
- 上面两个方法中,我们定义了两个方法
add()
和sub()
用于进行加减法运算。另外还定义了一个方法apply()
,这个方法比较特殊,它的参数可以接收方法作为参数,其中第一个参数func
参数就是方法类型的参数, x,y
两个参数是普通参数,这两个参数将传递到func()
方法中,最终执行func(x,y)
返回结果。
下面我们看这个apply
方法的使用。
1 | print(apply(add,3,4)) #执行结果: 7 |
从上面执行结果可以看出,通过调用apply()
方法,我们可以实现加减法运算。从执行结果上来看,和单独去调用这2个方法执行是一样的效果,只不过从执行方式上我们把这两个方法作为参数传到另外一个方法去执行了。
问题思考
基于上面的例子,如果我们现在新增一个需求:要求add
和sub
方法执行时打印日志,那么我们该如何处理,常规思维我们可能会直接在两个方法中加入日志打印语句如下所示:
1 | def add(x,y): |
- 当方法比较少时这样处理也没啥问题,但是当我们新增其他方法也需要加入日志打印时,还这样一个个加入就会显得代码很冗余,这显然违背人生苦短,我用Python的Python初衷。那么如何解决这个问题呢?
- 上面的案例中
apply()
方法我们曾经使用它来执行加减法方法的运算,它就像一个公共工具来承载两个方法的执行,那么我们现在需要在方法中增加公共内容,其实就可以直接在这个方法里面改造。
1 | def apply(func,x,y): |
在上面
apply
方法中我们增加了一个日志语句,这样以后每次执行add
和sub
方法时就会执行日志打印语句,从而减少在add,sub
方法中直接写入日志打印语句,这样就可以精简代码,提高效率。看似一切都比较完美了,但是还是有一个问题:每次执行都得通过
apply
这个方法去执行,可能会违背业务逻辑,比如我封装某个方法要调用加法运算,却每次得通过apply
这个中转方法来执行,会对代码的可读性造成影响。那么有没有有一种方法既可以使
add
和sub
方法都具有日志打印功能,而且可以直接调用原方法就能实现的两全其美的方法呢?有!这个时候装饰器就开始闪亮登场了!
初识装饰器
首先我们来看一个例子,然后来解释什么是装饰器。
1 | import logging |
- 从上面的例子我们可以看到定义了一个方法
logging_tool
参数可以接收方法 在这个方法里面还额外定义了一个方法wrap
参数为2个普通参数x,y
- 在
wrap
参数内部定义了日志打印语句,然后是返回传入方法和参数,执行原方法的运算结果。 - 最后在
logging_tool
这个方法里面返回wrap
方法,这个方法即支持了日志打印功能和原方法add
的求和功能。 add=logging_tool(add)
其实就是一个装饰赋值的过程,logging_tool
就是一个装饰器,经过logging_tool
方法装饰之后,新的add
的方法便具有日志打印功能和求和运算功了,我们再次调用装饰之后的add
方法add(3,4)
便可得到如下结果:
1 | 7 |
装饰器语法糖
在python中相信使用过unittest
或pytest
单元测试框架的同学对于 @
符号一定不会陌生,
比如我们经常在unittest中会看到如下类似的用法:
1 |
|
上面的@
符号其实就是一个装饰器符号,它可以代替上面的add=logging_tool(add)
赋值过程。那么我们使用语法糖@
符号来改造代码如下:
1 | import logging |
- 从上面的例子我们可以看出使用
@
装饰符号更加简洁明了,既不影响add
的方法定义结构,也支持了日志打印功能,提高代码复用效率,当有新的方法需要日志打印功能时,同样可以继续在方法顶部加上@logging_tool
即可。 - 装饰器本质上是一个Python函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外的功能,装饰器的返回值也是一个函数/类对象。
- 它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。
- 有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码到装饰器中并继续重用。
简单来说:装饰器的作用就是让已经存在的对象添加额外的功能。
*args **kwargs
- 上面装饰器中我们是根据
add
的参数也在装饰器wrap
方法中定义了两个参数,但是如果新的业务方法有三个参数或者更多那么该如何处理,难道定义多个不同的装饰器吗?显然这样是不合理的,这里我们可以使用不定参数的处理方式,关于*args
和**kwargs
可以参考:Python *args和**kwargs 这篇文章。 - 我们将上面的装饰器
loging_tool
进行参数改造代码如下:
1 | def logging_tool(func): #日志装饰器 |
通过上面的改造,不过被装饰的业务参数是多少,我们都可以接收处理,这样极大的提高了装饰器的灵活性。
装饰器参数
- 装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的方法
add
。如果现在我要根据日志不同级别来分别打印日志,那么该如何处理?先看如下代码:
1 |
|
执行结果:
1 | 6 |
- 从上面例子我们可以看出,首先将装饰器增加了参数
level
,然后增加了一个方法decorator
用来接收业务方法,在wrap
内部根据装饰器参数level
来进行判断输入不同级别的日志。 - 在调用装饰器时,同样也会传入参数
level=“warn”
functools.wraps
使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring
、__name__、
参数列表,先看例子:
1 | def logging_tool(func): #日志装饰器 |
执行结果:
1 | wrap |
通过上面的例子我们不难发现,原本add
方法的docstring
、__name__、
属性都被装饰器里面的wrap
方法的属性替换了,显然这个不是我们愿意看到的。不过,我们可以通过 @functools.wraps
将原函数的元信息拷贝到装饰器里面的 func
函数中,使得装饰器里面的 func
和原函数有一样的元信息。
1 | import functools |
执行结果:
1 | add |
我们可以从上面的例子看到,使用 @functools.wraps
后将方法的元信息得到了保留。
类装饰器
装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的call
方法,当使用 @
形式将装饰器附加到函数上时,就会调用此方法。
1 | class Foo(): |
执行结果
1 | 类装饰器开始执行 |
内置装饰器
Python内置了一些装饰器,如:@property
, @classmethod
, @staticmethod
接下来我们一一阐述这些内置装饰器的用法。
@property
property装饰器是负责把一个方法变成属性,
1 | class Student(object): |
从上面的例子我们可以看出,使用装饰器@property
我们将score
方法变成了一个类的属性,另外又通过@score.setter
变成一个可以赋值的属性,如果不定义的话score
就只是一个只读的属性。
@classmethod
@classmethod 类方法:定义备选构造器,第一个参数是类本身(参数名不限制,一般用cls)
我们结合大家熟悉的unittest
测试框架看下面这个例子
1 | import unittest |
执行结果
1 | test class start ..... |
- 上面案例中我们使用
@classmethod
来装饰setUpClass
和tearDownClass
两个方法,这两个方便便成为类方法,也就是Test
类的所有test
类型用例执行时都会执行setUpClass
和tearDownClass
两个方法里面的内容。 - 这样在实际应用场景中比如进行数据库初始化连接断开操作就非常方便了,不用每个用例方法单独去调用或者单独写初始化方法。
@staticmethod
@staticmethod表示静态方法,静态方法和普通方法不同之处在于,静态方法属于类,无需实例化便可进行调用。
1 | class C(object): |
上面我们通过@staticmehod
定义了一个静态方法f()
在调用时可以直接调用,无需实例化。