如果有AOP的编程经验,理解Python的装饰器就是分分钟的事。既然是装饰器,那么对被装饰的对象来说,一定是功能得到了增强,按方法能增强的地方进行划分,又可以分为以下四类:
1. 方法调用前;
2. 方法调用后;
3. 方法调用前后(环绕);
4. 方法调用异常;
我们以一个简单的加法运算来进行说明,按方法的增强点依次进行功能增强,首先看加法的代码,非常简单:
def add (a, b):
return a + b
在调用之前检查方法的参数是否合规,或者判断是否有相应的执行权限,这都是常用的手法,本例中检验第一个参数是否为正数,如果是,则执行加法,否则抛出错误,那我们可以定义如下的装饰器:
def before(func):
# 只能通过参数声明的方式获取参数
def check(a, *args):
# 如果小于0,抛出异常
if(a < 0):
raise Exception('a is less than zero!')
else:
return func(a, *args)
# 记住,返回的一定是函数
return check
在这里,最难的就是怎么获取方法的参数,不要期待有反射的方式,更不要企图通过方法的属性可以获取到参数(这句话我说错了,还真有这样的方法),一定要通过变长参数的方式(元组与字典)才能获取到所有的实参。
现在继续执行添加注解与调用方式,就会出现你想要的结果,如下:
@before
def add (a, b):
return a + b
# 自动会抛出错误
print add(-1, 2)
这一般用于对结果进行处理,如类型转换,将对象转换为JSON字符串,或者结果判断等,如下:
# 携带参数的写法
def after (num):
# 注意嵌套层次
def afterProxy(func):
# 修改返回结果
def addMore (*args):
# 对结果进行包装
result = func(* args) + num
return result
return addMore
return afterProxy
请记住上面的调用层次,一定是先执行参数的方法,返回没有参数的装饰函数,最后返回封装后的结果函数,等同于after(5)(before(add(a, b))),调用方式如下:
# 等同于after(5)(before(add(a, b)))
# 可以不同时使用,装饰之间没有依赖关系
@after(5)
@before
def add (a, b):
return a + b
# 返回结果为8
print add(1, 2)
这个方式应用场景最多,比如事务的管理、数据库连接的自动开始与关闭等,并且前两项装饰器能干的事情它都能干,所以也是最强大的装饰器。原理也很简单,就是在方法执行前做一些事情,在方法执行后做一些事情,如下:
# 定义环绕装饰
def round(func):
def roundProxy(*args):
print 'before execute'
print func(*args)
print 'end execute'
return roundProxy
# 执行调用
add(1, 2)
# 输出结果为
# before execute
# 8
# end execute
这种装饰器一般用于方法调用出现异常时,所以一定会携带装饰器参数,且参数为异常类型,然后处理逻辑会放在异常捕获语句中,示例如下:
# 定义异常装饰器
# ex为异常类型
def mathExcept(ex):
def exceptProxy(func):
def divFunc(*args):
try:
return func(*args)
# 捕获异常
except ex:
# 处理业务逻辑
return 0
return divFunc
return exceptProxy
# 添加异常装饰器
@mathExcept(ZeroDivisionError)
def div(a, b):
if(b == 0):
raise ZeroDivisionError('b is zero')
else:
return a / b
# 调用结果
print div(3, 0)
经过异常装饰器处理后,你会返现,现在3除以0也不报错了,而是返回0,所以这适合统一的异常处理。
经过上面的例子,我们明白,装饰器就是一个个的函数,而它们声明的顺序也就是函数调用的顺序,所以如果它们对函数的返回结果与参数非常敏感,那么一定要仔细斟酌它们的顺序,以上面的例子为例,如果将装饰器的顺序改为如下,则会出现参数合规错误:
@after(5)
@before
@round
def add (a, b):
return a + b
为什么呢?因为执行round函数后,返回结果与参数都为None,所以执行after函数必然出错。
在很多权威的书籍里,都推荐大家使用@wraps注解,这样能保留方法的原始信息,例如“name”,“doc”等,甚至还能wrapped属性对包装的函数进行还原(据我测试多次,无法实现,版本2.7.14),写法如下:
# 引入wraps
from functools import wraps
def before(func):
@wraps(func)
def check(a, *args):
# 如果小于0,抛出异常
if(a < 0):
raise Exception('a is less than zero!')
else:
return func(a, *args)
# 记住,返回的一定是函数
return check
@before
def add (a, b):
return a + b
# 现在可以获取函数的元数据信息
print add.__name__
print add.__doc__