内容概要
内容详细: 装饰器介绍 为何要用装饰器 1 2 3 4 5 6 软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的 软件包含的所有的功能的源代码以及调用方式,都应该避免修改,否则一旦改错,极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器
什么是装饰器 1 2 3 4 5 6 7 8 9 10 '装饰' :代指为被装饰对象添加新的功能'器' :代指器具/工具装饰器与被装饰的对象均可以是任意可调用对象 装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景 装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用 ps:可调用对象有函数、方法或者类
装饰器的实现 函数装饰器分为:’无参装饰器’和’有参装饰器’两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物
无参装饰器的实现 1 2 3 4 5 6 7 8 9 10 如果想为下述函数添加统计其执行时间的功能 import timedef index (): time.sleep(3 ) print ('Welcome to the index page’) return 200 index() #函数执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样: strat_time = time.time() index() stop_time = time.time() print ('run time is %s' %(stop_time-start_time)) 考虑到还有可能要统计其他函数的执行时间,于是我们将其做成一个单独的工具,函数体需要外部传入被装饰的函数从而进行调用,我们可以使用参数的形式传入 def wrapper (func ): start_time=time.time() res=func() stop_time=time.time() print ('run time is %s' %(stop_time-start_time)) return res 但之后函数的调用方式都需要统一改成: wrapper(index) wrapper(其他函数)
1 2 3 4 5 6 7 8 9 10 11 12 ======================================================================================== 但这就违反了不能修改被装饰对象调用方式的原则,于是我们换一种为函数体传值的方式,即将值包给函数,如下: def timer (func ): def wrapper (): start_time=time.time() res=func() stop_time=time.time() print ('run time is %s' %(stop_time-start_time)) return res return wrapper
1 2 3 4 5 这样我们便可以在不修改被装饰函数源代码和调用方式的前提下,为其添加上统计时间的功能,只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名/函数名index 如下: index = timer(index) index()
1 2 3 4 5 6 7 8 9 10 11 12 13 ======================================================================================== 至此我们便实现了一个无参装饰器timer,可以在不修改被装饰对象index源代码和调用方式的前提下为其加上新功能 但我们忽略了若被装饰的函数是一个有参函数,便会抛出异常 def home (name ): time.sleep(5 ) print ('Welcome to the home page' ,name) home=timer(home) home('egon' ) TypeError: wrapper() takes 0 positional arguments but 1 was given
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ======================================================================================== 之所以会抛出异常,是因为home('egon' )调用的其实是wrapper('egon' ),而函数wrapper没有参数 wrapper函数接收的参数其实是给最原始的func用的,为了能满足被装饰函数参数的所有情况 要使用:*args + **kwargs的组合,于是 ---> 最终版装饰器timer如下: def timer (func ): def wrapper (*args,**kwargs ): start_time=time.time() res=func(*args,**kwargs) stop_time=time.time() print ('run time is %s' %(stop_time-start_time)) return res return wrapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 此时我们就可以用timer来装饰带参数或不带参数的函数了,但是为了简洁而优雅地使用装饰器 python提供了专门的装饰器语法来取代index = timer(index)的形式 当解释器解释到@timer时就会调用timer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名 @timer def index (): time.sleep(3 ) print ('Welcome to the index page' ) return 200 @timer time.sleep(5 ) print ('Welcome to the home page’,name) 如果我们有多个装饰器,可以叠加多个 @deco3 @deco2 @deco1 def index(): pass 叠加多个装饰器也无特殊之处,上述代码语义如下: index=deco3(deco2(deco1(index)))
有参装饰器的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 在了解了无参装饰器的实现原理之后,我们可以再实现一个用来为被装饰对象添加认证功能的装饰器,实现的基本形式如下: def deco (func ): def wrapper (*args,**kwargs ): 编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res return wrapper 如果我们想提供多种不同的认证方式以供选择,单从wrapper函数的实现角度改写如下 def deco (func ): def wrapper (*args,**kwargs ): if driver == 'file' : 编写基于文件的认证,认证通过则执行res=func(*args,**kwargs),并返回res elif driver == 'mysql' : 编写基于mysql认证,认证通过则执行res=func(*args,**kwargs),并返回res return wrapper
1 2 3 4 5 6 函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能,不能用来接受其他类别的参数,可以再deco的外部再包一层函数auth,用来专门接受额外的参数,这样便保证了再auth函数内无论多少层都可以引用到 def auth (driver ): def deco (func ): …… return deco
1 2 3 4 5 6 7 8 9 10 此时我们就实现了一个有参装饰器,使用方式如下 先调用auth_type(driver='file' ),得到@deco,deco是一个闭包函数, 包含了对外部作用域名字driver的引用,@deco的语法意义与无参装饰器一样 @auth(driver='file' ) def index (): pass @auth(driver='mysql' ) def home (): pass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 可以使用help (函数名)来查看函数的文档注释,本质就是查看函数的doc属性,但对于被装饰之后的函数,查看文档注释 @timer def home (name ): ''' home page function :param name: str :return: None ''' time.sleep(5 ) print ('Welcome to the home page' ,name) print (help (home))打印结果: Help on function wrapper in module __main__: wrapper(*args, **kwargs) None
1 2 3 4 5 6 7 8 9 10 11 12 13 在被装饰之后home=wrapper,查看home.name也可以发现home的函数名确实是wrapper,想要保留原函数的文档和函数名属性,需要修正装饰器 def timer (func ): def wrapper (*args,**kwargs ): start_time=time.time() res=func(*args,**kwargs) stop_time=time.time() print ('run time is %s' %(stop_time-start_time)) return res wrapper.__doc__=func.__doc__ wrapper.__name__=func.__name__ return wrapper
1 2 3 4 5 6 7 8 9 10 11 12 13 按照上述方式来实现保留原函数属性过于麻烦,functools模块下提供一个装饰器wraps专门用来帮我们实现这件事,用法如下 from functools import wrapsdef timer (func ): @wraps(func ) def wrapper (*args,**kwargs ): start_time=time.time() res=func(*args,**kwargs) stop_time=time.time() print ('run time is %s' %(stop_time-start_time)) return res return wrapper