Python装饰器

装饰器前缘

Python装饰器对于Python初学者可能是一个比较抽象的概念,在了解装饰器之前,我们必须先熟悉Pythond方法的一个特性:Python中的方法可以像普通变量一样当做参数传递给另外一个方法,我们来看一个例子:

1
2
3
4
5
6
7
8
def add(x,y):
return x+y

def sub(x,y):
return x-y

def apply(func,x,y):
return func(x,y)
  • 上面两个方法中,我们定义了两个方法 add()sub()用于进行加减法运算。另外还定义了一个方法apply(),这个方法比较特殊,它的参数可以接收方法作为参数,其中第一个参数func参数就是方法类型的参数,
  • x,y两个参数是普通参数,这两个参数将传递到func()方法中,最终执行func(x,y)返回结果。
    下面我们看这个apply方法的使用。
1
2
print(apply(add,3,4))  #执行结果: 7
print(apply(sub,3,4)) #执行结果:-1

从上面执行结果可以看出,通过调用apply()方法,我们可以实现加减法运算。从执行结果上来看,和单独去调用这2个方法执行是一样的效果,只不过从执行方式上我们把这两个方法作为参数传到另外一个方法去执行了。

问题思考

基于上面的例子,如果我们现在新增一个需求:要求addsub方法执行时打印日志,那么我们该如何处理,常规思维我们可能会直接在两个方法中加入日志打印语句如下所示:

1
2
3
4
5
6
7
def add(x,y):
logging.info('add logging is run' )
return x+y

def sub(x,y):
logging.info('sub logging is run' )
return x-y
  • 当方法比较少时这样处理也没啥问题,但是当我们新增其他方法也需要加入日志打印时,还这样一个个加入就会显得代码很冗余,这显然违背人生苦短,我用Python的Python初衷。那么如何解决这个问题呢?
  • 上面的案例中apply()方法我们曾经使用它来执行加减法方法的运算,它就像一个公共工具来承载两个方法的执行,那么我们现在需要在方法中增加公共内容,其实就可以直接在这个方法里面改造。
1
2
3
4
5
6
def apply(func,x,y):
logging.info('%s logging is run' %func.__name__)
return func(x,y)

print(apply(add,3,4))
print(apply(sub,3,4))
  • 在上面apply方法中我们增加了一个日志语句,这样以后每次执行addsub方法时就会执行日志打印语句,从而减少在add,sub方法中直接写入日志打印语句,这样就可以精简代码,提高效率。
  • 看似一切都比较完美了,但是还是有一个问题:每次执行都得通过apply这个方法去执行,可能会违背业务逻辑,比如我封装某个方法要调用加法运算,却每次得通过apply这个中转方法来执行,会对代码的可读性造成影响。

  • 那么有没有有一种方法既可以使addsub方法都具有日志打印功能,而且可以直接调用原方法就能实现的两全其美的方法呢?有!这个时候装饰器就开始闪亮登场了!

初识装饰器

首先我们来看一个例子,然后来解释什么是装饰器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s %(filename)s[line:%(lineno)d]%(levelname)s%(message)s')

def add(x,y):
return x+y

def logging_tool(func): #日志装饰器
def wrap(x,y):
logging.info(' %s logging is run' % func.__name__)
return func(x,y)
return wrap

add=logging_tool(add)
print(add(3,4))
  • 从上面的例子我们可以看到定义了一个方法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
2
7
2019-07-06 09:08:56,325 decorator_blog.py[line:10]INFO add logging is run

装饰器语法糖

在python中相信使用过unittestpytest单元测试框架的同学对于 @符号一定不会陌生,
比如我们经常在unittest中会看到如下类似的用法:

1
2
3
4
5
6
7
8
@unittest.skip("skip Test2")
class Test2(unittest.TestCase):
def setUp(self):
print("Test2 start")

@classmethod
def setUpClass(cls):
print("Class module start test>>>>>>>>")

上面的@符号其实就是一个装饰器符号,它可以代替上面的add=logging_tool(add)赋值过程。那么我们使用语法糖@符号来改造代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s %(filename)s[line:%(lineno)d]%(levelname)s%(message)s')


def logging_tool(func): #日志装饰器
def wrap(x,y):
logging.info(' %s logging is run' % func.__name__)
return func(x,y)
return wrap


@logging_tool
def add(x,y):
return x+y

print(add(3,4))
  • 从上面的例子我们可以看出使用@装饰符号更加简洁明了,既不影响add的方法定义结构,也支持了日志打印功能,提高代码复用效率,当有新的方法需要日志打印功能时,同样可以继续在方法顶部加上@logging_tool即可。
  • 装饰器本质上是一个Python函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外的功能,装饰器的返回值也是一个函数/类对象。
  • 它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。
  • 有了装饰器,我们就可以抽离出大量与函数功能本身无关的代码到装饰器中并继续重用。
    简单来说:装饰器的作用就是让已经存在的对象添加额外的功能。

*args **kwargs

  • 上面装饰器中我们是根据add的参数也在装饰器wrap方法中定义了两个参数,但是如果新的业务方法有三个参数或者更多那么该如何处理,难道定义多个不同的装饰器吗?显然这样是不合理的,这里我们可以使用不定参数的处理方式,关于*args**kwargs 可以参考:Python *args和**kwargs 这篇文章。
  • 我们将上面的装饰器loging_tool进行参数改造代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
def logging_tool(func): #日志装饰器
def wrap(*args,**kwargs):
logging.info(' %s logging is run' % func.__name__)
return func(*args,**kwargs)
return wrap

@logging_tool
def add_multi(x,y,z):
return x+y+z


print(add_multi(1,2,3))

通过上面的改造,不过被装饰的业务参数是多少,我们都可以接收处理,这样极大的提高了装饰器的灵活性。

装饰器参数

  • 装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的方法add。如果现在我要根据日志不同级别来分别打印日志,那么该如何处理?先看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

def logging_tool(level): #日志装饰器
def decorator(func):
def wrap(*args,**kwargs):
if level=='info':
logging.info(' %s info logging is run' % func.__name__)
elif level=='warn':
logging.info(' %s warn logging is run' % func.__name__)
else:
logging.debug(' %s debug logging is run' % func.__name__)
return func(*args,**kwargs) #返回原业务方法结果
return wrap #返回增加装饰器的功能
return decorator #返回整个结果


@logging_tool(level="warn")
def add_multi(x,y,z):
return x+y+z
print(add_multi(1,2,3))

执行结果:

1
2
6
2019-07-06 10:39:25,698 decorator_blog.py[line:22]INFO add_multi warn logging is run
  • 从上面例子我们可以看出,首先将装饰器增加了参数level,然后增加了一个方法decorator用来接收业务方法,在wrap内部根据装饰器参数level来进行判断输入不同级别的日志。
  • 在调用装饰器时,同样也会传入参数level=“warn”

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring__name__、参数列表,先看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def logging_tool(func): #日志装饰器
def wrap(x,y):
'''logging tool wrap'''
logging.info(' %s logging is run' % func.__name__)
return func(x,y)
return wrap

@logging_tool
def add(x,y):
'''
add function
:return:
'''
return x+y


print(add.__name__)
print(add.__doc__)

执行结果:

1
2
wrap
logging tool wrap

通过上面的例子我们不难发现,原本add方法的docstring__name__、属性都被装饰器里面的wrap方法的属性替换了,显然这个不是我们愿意看到的。不过,我们可以通过@functools.wraps 将原函数的元信息拷贝到装饰器里面的func函数中,使得装饰器里面的func和原函数有一样的元信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import functools

def logging_tool(func): #日志装饰器
@functools.wraps(func) #保留方法的元信息
def wrap(x,y):
'''logging tool wrap'''
logging.info(' %s logging is run' % func.__name__)
return func(x,y)
return wrap


@logging_tool
def add(x,y):
'''add function'''
return x+y


print(add.__name__)
print(add.__doc__)

执行结果:

1
2
add
add function

我们可以从上面的例子看到,使用 @functools.wraps 后将方法的元信息得到了保留。

类装饰器

装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的call方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Foo():
def __init__(self,func):
self._func=func

def __call__(self, *args, **kwargs):
print('类装饰器开始执行')
self._func(*args,**kwargs)
print('类装饰器执行结束')


@Foo
def bar():
print('hello world')

bar()

执行结果

1
2
3
类装饰器开始执行
hello world
类装饰器执行结束

内置装饰器

Python内置了一些装饰器,如:@property, @classmethod, @staticmethod 接下来我们一一阐述这些内置装饰器的用法。

@property

property装饰器是负责把一个方法变成属性,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Student(object):

@property
def score(self):
return self._score

@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value


s=Student()
s.score=90
print(s.score) #执行结果 90

s.score=101
print(s.score) #执行结果:score must between 0 ~ 100!

s.score='hello'
print(s.score) #执行结果:score must be an integer!

从上面的例子我们可以看出,使用装饰器@property我们将score方法变成了一个类的属性,另外又通过 @score.setter 变成一个可以赋值的属性,如果不定义的话score就只是一个只读的属性。

@classmethod

@classmethod 类方法:定义备选构造器,第一个参数是类本身(参数名不限制,一般用cls)
我们结合大家熟悉的unittest测试框架看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import unittest
class Test(unittest.TestCase):

@classmethod #类方法装饰器
def setUpClass(cls):
print("test class start .....")

@classmethod
def tearDownClass(cls):
print("test class end ....")

def setUp(self):
print("Test case is testing")

def tearDown(self):
print("test case is end!")

def test_case(self):
print("test_case is runing!")

if __name__=='__main__':
unittest.main()

执行结果

1
2
3
4
5
6
7
8
9
test class start .....
Test case is testing
----------------------------------------------------------------------
test_case is runing!
Ran 1 test in 0.000s
test case is end!

test class end ....
OK
  • 上面案例中我们使用@classmethod来装饰setUpClasstearDownClass两个方法,这两个方便便成为类方法,也就是Test类的所有test类型用例执行时都会执行setUpClasstearDownClass两个方法里面的内容。
  • 这样在实际应用场景中比如进行数据库初始化连接断开操作就非常方便了,不用每个用例方法单独去调用或者单独写初始化方法。

@staticmethod

@staticmethod表示静态方法,静态方法和普通方法不同之处在于,静态方法属于类,无需实例化便可进行调用。

1
2
3
4
5
6
7
class C(object):
@staticmethod
def f():
print('hello world')


C.f() #运行结果hello world

上面我们通过@staticmehod定义了一个静态方法f() 在调用时可以直接调用,无需实例化。

参考资料