前面我们学习了闭包,闭包从语法上来讲就是在函数体内定义一个函数,而本节课我们要讲的装饰器就是利用闭包语法来实现项目的简洁化和模式化。
装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
由于项目越来越庞大,有时候客户反映程序用起来不像以前那么顺,这时候,技术负责人想让你编程对项目中每个函数进行性能测试, 此时,作为一线搬砖的你应该怎么办?
首先,假如我们项目中有一些函数,如下:
# 函数 1 def func_one(data): for i in range(1, data): data += i return data # 函数 2 def func_two(data): for i in range(1, data): data -= i return data # 函数 3 def func_three(data): for i in range(1, data): data *= i return data
现在我们想看看测试这个函数的性能,于是我们改写以上代码如下:
from time import time ''' 项目已有函数 ''' # 函数 1 def func_one(data): for i in range(1, data): data += i return data # 函数 2 def func_two(data): for i in range(1, data): data -= i return data # 函数 3 def func_three(data): for i in range(1, data): data *= i return data ''' 对项目已有函数的性能测试 ''' before = time() func_one(25000) after = time() print("该函数耗时:", (after - before)) before = time() func_two(25000) after = time() print("该函数耗时:", (after - before)) before = time() func_three(25000) after = time() print("该函数耗时:", (after - before))
作为一个懒惰的程序员,我们立马就发现了,有一些重复的代码,像喋喋不休的排比句一样反复出现:执行一个函数,并计算这个函数的执行时间。于是我们就可以把这个模式抽象出来,改写代码如下:
from time import time ''' 项目已有函数 ''' # 函数 1 def func_one(data): for i in range(1, data): data += i return data # 函数 2 def func_two(data): for i in range(1, data): data -= i return data # 函数 3 def func_three(data): for i in range(1, data): data *= i return data ''' 对项目已有函数的性能测试 ''' def timer(func, data): before = time() func(data) after = time() return(after - before) print("该函数耗时:", timer(func_one, 25000)) print("该函数耗时:", timer(func_two, 25000)) print("该函数耗时:", timer(func_three, 25000))
但这样还是很麻烦,因为我们得改到所有的测试用例,比如把 func_one(25000) 改成 timer(add, 25000) 等等。于是我们进一步改进,让 timer 返回函数:
from time import time ''' 项目已有函数 ''' # 函数 1 def func_one(data): for i in range(1, data): data += i return data # 函数 2 def func_two(data): for i in range(1, data): data -= i return data # 函数 3 def func_three(data): for i in range(1, data): data *= i return data ''' 对项目已有函数的性能测试 ''' def timer(func): def wraper(data): before = time() rst = func(data) after = time() print("该函数耗时:", after - before) return rst return wraper func_one = timer(func_one) func_two = timer(func_two) func_three = timer(func_three) func_one(25000) func_two(25000) func_three(25000)
这里的最后一个问题是,我们的 timer 包装的函数可能有不同的参数,于是我们可以进一步用 *args, **kwargs 来传递参数,只需要把上面的 timer 函数改成下面的形式即可:
def timer(func): def wraper(*args, **kwargs): before = time() rst = func(*args, **kwargs) after = time() print("该函数耗时:", after - before) return rst return wraper
这里的 timer 函数就是一个“装饰器”,它接受一个函数,并返回一个新的函数。在装饰器的内部,对原函数进行了“包装”。
大家注意,我们调用代码 func_one = timer(func_one)
之后,实际上相当于改写了
func_one 函数,原项目中的关于 func_one 函数的调用都多了一个对 func_one 函数的进行性能测试的功能,而对
func_one 函数本身的返回值并无影响。当然,我们可以把性能测试的输出写到日志里面,而不是输出到屏幕上。
这种不需要改写原函数本身,就可以增加原函数行为的方式非常符合软件工程中的“对增加开发,对修改关闭”的设计原则。
刚刚我们利用闭包的语法解决了项目中关于对所有函数进行性能测试的问题,实际上,我们自己写了一个装饰器,但是我们都知道,Python 是有很高的装逼姿势,python 提供了一个装饰器的语法糖,看起来很有逼格。
当然,我们也不能为了装逼而装逼,凡事总得有个理由吧,那我们就找一个,我们发现我们自己写的装饰器代码,最后调用部分代码如下:
func_one = timer(func_one) func_two = timer(func_two) func_three = timer(func_three) func_one(25000) func_two(25000) func_three(25000)
上面这个语句里,我们把要测试性能的每个函数的名字重复了 3 次,如果项目中函数改了名字,我们的性能测试代码就得改 3 处。装逼的 Python 就想了一个更“好”的方法,提供了一个语法来替换上面的内容:
from time import time def timer(func): def wraper(*args, **kwargs): before = time() rst = func(*args, **kwargs) after = time() print("该函数耗时:", after - before) return rst return wraper # 函数 @timer def func_one(data): for i in range(1, data): data += i return data # 函数 2 @timer def func_two(data): for i in range(1, data): data -= i return data # 函数 3 @timer def func_three(data): for i in range(1, data): data *= i return data func_one(25000) func_two(25000) func_three(25000)
我们发现,使用了装逼的语法糖后,我们对每个要测试性能的函数只需要调用一次即可。使用语法糖后的装饰性和我们自己写的装饰器完全等价,只是 @ 写法更简洁一些。
如果我们想给装饰器传参数应该怎么办?比如我们想把每个要测试性能函数的函数名传给装饰器,其实很简单,我们给测试函数性能的装饰器再装饰一下就可以了。
from time import time def timer(funcname): def decorate(func): def wraper(*args, **kwargs): before = time() rst = func(*args, **kwargs) after = time() print(funcname + "耗时:", after - before) return rst return wraper return decorate # 函数 @timer("func_one函数") def func_one(data): for i in range(1, data): data += i return data # 函数 2 @timer("func_two函数") def func_two(data): for i in range(1, data): data -= i return data # 函数 3 @timer("func_three函数") def func_three(data): for i in range(1, data): data *= i return data func_one(25000) func_two(25000) func_three(25000)
一个函数还可以同时定义多个装饰器,装饰器的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,比如以下函数有三个装饰器,该装饰器函数等同于:f = a(b(c(f)))
。
@a @b @c def f(): pass
装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的 __call__ 方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法,大家学完面向对象之后,可以再看如下案例。
from time import time class Timer(object): def __init__(self, func): self._func = func def __call__(self, arg): before = time() rst = self._func(arg) after = time() print("该函数耗时:", after - before) return rst @Timer def func_one(data): for i in range(1, data): data += i return data func_one(25000)
会使用基本语法写装饰器。
会使用装饰器语法写装饰器。
会给装饰器传参数。
著名公司(某东)笔试题:创建一个装饰器把下面函数输出的字符串首字母大写。
def greetings(word="hi jack 马,I'm you brother dongdong"): return word.lower()