简介
装饰器顾名思义就是负责装饰的函数。一个形象的理解是,装饰器就像女人用的化妆品。白天要出门的时候,就要用化妆品这种装饰器去打扮一番,效果是变得年轻漂亮、精神奕奕,而本质属性不会发生变化,比如性别不会变;到了晚上就要卸妆,卸妆后本质属性也不会发生变化;化妆品这种东西可以重复使用,并且可以画出各种不同的效果。装饰器也具有相同的特点,能够增加现有函数各种新的功能。凡是需要使用重复功能的代码,并且这些代码与核心功能的代码没有直接关联的都可以使用装饰器来代替。可以想象装饰器的使用场合非常广泛,比如一个封装好了的代码块,我们想在代码块运行之后打印出日志信息,就可以通过装饰器增加一个收集日志并打印的功能。
接下来我会通过一个简单的例子讲解装饰器的使用。
一个计算函数
一天,老板跟我说需要我写一个计算任意数字的平方值的程序(嘿嘿,难度很大呀),花了半天时间终于写出来了:
1 2 |
def cal_num(num): return num ** 2 |
终于可以交差了。满怀得意的把这个程序交给老板,老板测试了下:
1 2 3 4 |
>>>cal_num(2) 4 >>>cal_num(3) 9 |
嗯,结果是对的,但是打印的结果不美观,不能吸引到客户。你再回去提炼提炼下,看能不能达到这样的效果:
1 2 |
>>>cal_num(2) *****4***** |
这个效果实现起来应该不难,于是回去后又花了半天时间,写出如下代码:
1 2 3 |
def cal_num(num): value = num ** 2 return '{:*^11}'.format(value) |
这的确能达到老板的要求,但是重新修改核心计算的代码后,可能会产生察觉不了的bug。为了保险起见,我选择使用装饰器,增加一个格式化计算结果的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 定义一个格式化计算结果的函数 def format_string(func): def wrapper(num): return '{num:*^11}'.format(num=func(num)) return wrapper # 核心计算函数不变 def cal_num(num): return num ** 2 cal_num = format_string(cal_num) print(cal_num(2)) # 结果为 *****4***** |
在上面的代码中创建了一个新的函数format_string()
,它的功能是将计算结果格式化。整个format_string()
函数就是装饰器的核心。我们详细地介绍下这个函数:
- 该函数中又定义了一个函数
wrapper()
,这个函数能实现格式化功能,但是需要传入数值给关键字num
; return wrapper
语句使得wrapper()
函数变成一个闭包函数。- 当使用
cal_num = format_string(cal_num)
语句时,可以把wrapper
看成cal_num
,因此打印cal_num(2)
就是打印wrapper(2)
函数; - 注意这里的
func=cal_num
,而不是func=cal_num()
,区别就在cal_num
是一个函数对象,而cal_num()
是函数执行的结果。
总之,外层函数接受核心代码的函数对象(cal_num),内层的闭包函数接受需要传入核心代码的值。
装饰器
在这里,可以使用python语法糖@
:
1 |
@format_string # 等价于cal_num = format_string(cal_num) |
使用语法糖后,完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 定义一个格式化计算结果的函数 def format_string(func): def wrapper(num): return '{num:*^11}'.format(num=func(num)) return wrapper # 核心计算函数不变 @format_string def cal_num(num): return num ** 2 print(cal_num(2)) # 结果为 *****4***** |
@format_string
紧接着核心计算函数cal_num()
,从形式上也可以看出cal_num()
类似wrapper()
。这下实现了老板想要的功能,可以愉快地去交差了。还没等我开口,老板就说上次说的那种效果不好,可不可以改成这种效果:
1 |
$$$$$4$$$$$ |
老板的要求还是要答应的,谁让他是老板。于是这次花个几分钟时间写了如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 定义一个格式化计算结果的函数 def format_string(func): def wrapper(num): return '{num:$^11}'.format(num=func(num)) return wrapper # 核心计算函数不变 @format_string def cal_num(num): return num ** 2 print(cal_num(2)) # 结果为 $$$$$4$$$$$ |
带参数的装饰器
这次写完代码并没有立刻去交差,谁让老板这么难搞,万一又要变换效果呢?顿了顿,我觉得这样写不行,要能够将参数传给装饰器,就不用更改装饰器函数了,老板想要什么样式都可以实现。于是我们这样改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 定义一个格式化计算结果的函数 def format_string(str): def decorator(func): def wrapper(num): return '{num:{str}^11}'.format(num=func(num), str=str) return wrapper return decorator # 核心计算函数不变 @format_string('$') def cal_num(num): return num ** 2 print(cal_num(2)) # 结果为 $$$$$4$$$$$ |
在装饰器函数中又加了一层嵌套,从外到内各个函数传入:装饰器参数,核心函数,核心函数参数(str, func, num)。此时的cal_num()
还是类比wrapper()
,最外层的format_string
接受装饰器的参数,中间的decorator()
函数专门接受一个函数对象cal_num
。如果要换样式,直接修改语法糖中的参数就行。这样就实现了一个带参数可调变的格式化结果的装饰器函数。最后把这个程序交给老板完成任务。
总结
感觉装饰器很难的原因在于没有理清它的逻辑关系,本质上装饰器也是函数,但它是对核心程序的闭包封装,在原有的基础上增加更多的功能。细细回顾几遍上面的例子能够加深对装饰器的理解。