一 装饰器介绍
1.1 为何要用装饰器
软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。
软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。
开放封闭原则
开放:指的是对拓展功能是开放的
封闭:指的是对修改源代码是封闭的
装饰器就是在不修改被装饰器对象源代码以及调用方式的前提下为被装饰对象添加新功能
1.2 什么是装饰器
’装饰’代指为被装饰对象添加新的功能,’器’代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
提示:可调用对象有函数,方法或者类,此处我们单以本章主题函数为例,来介绍函数装饰器,并且被装饰的对象也是函数。
器指的是工具,可以定义成成函数
装饰指的是为其他事物添加额外的东西点缀
合到一起的解释:
装饰器指的定义一个函数,该函数是用来为其他函数添加额外的功能
二 装饰器的实现
函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。
# 一:储备知识 #1、 *args, **kwargs # def index(x,y): # print(x,y) # # # def wrapper(*args,**kwargs): # index(*args,**kwargs) # # # index(y=222,x=111) # wrapper(y=222,x=111) # 2、名称空间与作用域:名称空间的的"嵌套"关系是在函数定义阶段,即检测语法的时候确定的 # 3、函数对象: # 可以把函数当做参数传入 # 可以把函数当做返回值返回 # def index(): # return 123 # # def foo(func): # return func # # foo(index) # 4、函数的嵌套定义: # def outter(func): # def wrapper(): # pass # return wrapper # 闭包函数 # def outter(): # x=111 # def wrapper(): # x # return wrapper # # f=outter() # 传参的方式一:通过参数的形式为函数体传值 # def wrapper(x): # print(1) # print(2) # print(3) # x # # wrapper(1) # wrapper(2) # wrapper(3) # 传参的方式二:通过闭包的方式为函数体传值 # def outter(x): # # x=1 # def wrapper(): # print(1) # print(2) # print(3) # x # return wrapper # return outter内的wrapper那个函数的内地址 # # # f1=outter(1) # # f2=outter(2) # # f3=outter(3) # # # wrapper=outter(1)
2.1 无参装饰器的实现
如果想为下述函数添加统计其执行时间的功能
import time def index(): time.sleep(3) print('Welcome to the index page’) return 200 index() #函数执行
遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样
start_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(其他函数)
这便违反了不能修改被装饰对象调用方式的原则,于是我们换一种为函数体传值的方式,即将值包给函数,如下
def timer(func): def wrapper(): # 引用外部作用域的变量func start_time=time.time() res=func() stop_time=time.time() print('run time is %s' %(stop_time-start_time)) return res return wrapper
这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能,只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名 /函数名index,如下
index=timer(index) #得到index=wrapper,wrapper携带对外作用域的引用:func=原始的index index() # 执行的是wrapper(),在wrapper的函数体内再执行最原始的index
至此我们便实现了一个无参装饰器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
之所以会抛出异常,是因为home(‘egon’)调用的其实是wrapper(‘egon’),而函数wrapper没有参数。wrapper函数接收的参数其实是给最原始的func用的,为了能满足被装饰函数参数的所有情况,便用上args+*kwargs组合(见4.3小节),于是修正装饰器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
此时我们就可以用timer来装饰带参数或不带参数的函数了,但是为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代index=timer(index)的形式,需要在被装饰对象的正上方单独一行添加@timer,当解释器解释到@timer时就会调用timer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名
@timer # index=timer(index) def index(): time.sleep(3) print('Welcome to the index page') return 200 @timer # index=timer(home) def home(name): time.sleep(5) print('Welcome to the home page’,name)
如果我们有多个装饰器,可以叠加多个
@deco3 @deco2 @deco1 def index(): pass
叠加多个装饰器也无特殊之处,上述代码语义如下:
index=deco3(deco2(deco1(index)))
# 需求:在不修改index函数的源代码以及调用方式的前提下为其添加统计运行时间的功能 # def index(x,y): # time.sleep(3) # print('index %s %s' %(x,y)) # # index(111,222) # # index(y=111,x=222) # # index(111,y=222) # 解决方案一:失败 # 问题:没有修改被装饰对象的调用方式,但是修改了其源代码 # import time # # def index(x,y): # start=time.time() # time.sleep(3) # print('index %s %s' %(x,y)) # stop = time.time() # print(stop - start) # # index(111,222) # 解决方案二:失败 # 问题:没有修改被装饰对象的调用方式,也没有修改了其源代码,并且加上了新功能 # 但是代码冗余 # import time # # def index(x,y): # time.sleep(3) # print('index %s %s' %(x,y)) # # start=time.time() # index(111,222) # stop=time.time() # print(stop - start) # # # # start=time.time() # index(111,222) # stop=time.time() # print(stop - start) # # # start=time.time() # index(111,222) # stop=time.time() # print(stop - start) # 解决方案三:失败 # 问题:解决了方案二代码冗余问题,但带来一个新问题即函数的调用方式改变了 # import time # # def index(x,y): # time.sleep(3) # print('index %s %s' %(x,y)) # # def wrapper(): # start=time.time() # index(111,222) # stop=time.time() # print(stop - start) # # wrapper() # 方案三的优化一:将index的参数写活了 # import time # # def index(x,y,z): # time.sleep(3) # print('index %s %s %s' %(x,y,z)) # # def wrapper(*args,**kwargs): # start=time.time() # index(*args,**kwargs) # index(3333,z=5555,y=44444) # stop=time.time() # print(stop - start) # # # wrapper(3333,4444,5555) # # wrapper(3333,z=5555,y=44444) # 方案三的优化二:在优化一的基础上把被装饰对象写活了,原来只能装饰index # import time # # def index(x,y,z): # time.sleep(3) # print('index %s %s %s' %(x,y,z)) # # def home(name): # time.sleep(2) # print('welcome %s to home page' %name) # # # def outter(func): # # func = index的内存地址 # def wrapper(*args,**kwargs): # start=time.time() # func(*args,**kwargs) # index的内存地址() # stop=time.time() # print(stop - start) # return wrapper # # index=outter(index) # index=wrapper的内存地址 # home=outter(home) # home=wrapper的内存地址 # # # home('egon') # # home(name='egon') # 方案三的优化三:将wrapper做的跟被装饰对象一模一样,以假乱真 # import time # # def index(x,y,z): # time.sleep(3) # print('index %s %s %s' %(x,y,z)) # # def home(name): # time.sleep(2) # print('welcome %s to home page' %name) # # def outter(func): # def wrapper(*args,**kwargs): # start=time.time() # res=func(*args,**kwargs) # stop=time.time() # print(stop - start) # return res # # # # return wrapper # # 偷梁换柱:home这个名字指向的wrapper函数的内存地址 # home=outter(home) # # # res=home('egon') # res=wrapper('egon') # print('返回值--》',res) # 大方向:如何在方案三的基础上不改变函数的调用方式 # 语法糖:让你开心的语法 import time # 装饰器 # def timmer(func): # def wrapper(*args,**kwargs): # start=time.time() # res=func(*args,**kwargs) # stop=time.time() # print(stop - start) # return res # # # # return wrapper # # # # 在被装饰对象正上方的单独一行写@装饰器名字 # # @timmer # index=timmer(index) # def index(x,y,z): # time.sleep(3) # print('index %s %s %s' %(x,y,z)) # # # @timmer # home=timmer(ome) # def home(name): # time.sleep(2) # print('welcome %s to home page' %name) # # # index(x=1,y=2,z=3) # home('egon') # 思考题(选做),叠加多个装饰器,加载顺序与运行顺序 # @deco1 # index=deco1(deco2.wrapper的内存地址) # @deco2 # deco2.wrapper的内存地址=deco2(deco3.wrapper的内存地址) # @deco3 # deco3.wrapper的内存地址=deco3(index) # def index(): # pass # 总结无参装饰器模板 # def outter(func): # def wrapper(*args,**kwargs): # # 1、调用原函数 # # 2、为其增加新功能 # res=func(*args,**kwargs) # return res # return wrapper def auth(func): def wrapper(*args,**kwargs): # 1、调用原函数 # 2、为其增加新功能 name=input('your name>>: ').strip() pwd=input('your password>>: ').strip() if name == 'egon' and pwd == '123': res=func(*args,**kwargs) return res else: print('账号密码错误') return wrapper @auth def index(): print('from index') index()
# 偷梁换柱,即将原函数名指向的内存地址偷梁换柱成wrapper函数 # 所以应该将wrapper做的跟原函数一样才行 from functools import wraps def outter(func): @wraps(func) def wrapper(*args, **kwargs): """这个是主页功能""" res = func(*args, **kwargs) # res=index(1,2) return res # 手动将原函数的属性赋值给wrapper函数 # 1、函数wrapper.__name__ = 原函数.__name__ # 2、函数wrapper.__doc__ = 原函数.__doc__ # wrapper.__name__ = func.__name__ # wrapper.__doc__ = func.__doc__ return wrapper @outter # index=outter(index) def index(x, y): """这个是主页功能""" print(x, y) print(index.__name__) print(index.__doc__) # help(index) index(1, 2) # wrapper(1,2)
# 一、叠加多个装饰器的加载、运行分析(了解***) def deco1(func1): # func1 = wrapper2的内存地址 def wrapper1(*args,**kwargs): print('正在运行===>deco1.wrapper1') res1=func1(*args,**kwargs) return res1 return wrapper1 def deco2(func2): # func2 = wrapper3的内存地址 def wrapper2(*args,**kwargs): print('正在运行===>deco2.wrapper2') res2=func2(*args,**kwargs) return res2 return wrapper2 def deco3(x): def outter3(func3): # func3=被装饰对象index函数的内存地址 def wrapper3(*args,**kwargs): print('正在运行===>deco3.outter3.wrapper3') res3=func3(*args,**kwargs) return res3 return wrapper3 return outter3 # 加载顺序自下而上(了解) @deco1 # index=deco1(wrapper2的内存地址) ===> index=wrapper1的内存地址 @deco2 # index=deco2(wrapper3的内存地址) ===> index=wrapper2的内存地址 @deco3(111) # ===>@outter3===> index=outter3(index) ===> index=wrapper3的内存地址 def index(x,y): print('from index %s:%s' %(x,y)) # 执行顺序自上而下的,即wraper1-》wrapper2-》wrapper3 index(1,2) # wrapper1(1,2)
2.2 有参装饰器的实现
了解无参装饰器的实现原理后,我们可以再实现一个用来为被装饰对象添加认证功能的装饰器,实现的基本形式如下
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
函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能,不能用来接受其他类别的参数,可以在deco的外部再包一层函数auth,用来专门接受额外的参数,这样便保证了在auth函数内无论多少层都可以引用到
def auth(driver): def deco(func): …… return deco
此时我们就实现了一个有参装饰器,使用方式如下
先调用auth_type(driver='file'),得到@deco,deco是一个闭包函数,
包含了对外部作用域名字driver的引用,@deco的语法意义与无参装饰器一样
@auth(driver='file') def index(): pass @auth(driver='mysql') def home(): pass
可以使用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
在被装饰之后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
按照上述方式来实现保留原函数属性过于麻烦,functools模块下提供一个装饰器wraps专门用来帮我们实现这件事,用法如下
from functools import wraps def 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
# 一:知识储备 # 由于语法糖@的限制,outter函数只能有一个参数,并且该才是只用来接收 # 被装饰对象的内存地址 # def outter(func): # # func = 函数的内存地址 # def wrapper(*args,**kwargs): # res=func(*args,**kwargs) # return res # return wrapper # # # @outter # index=outter(index) # index=>wrapper # @outter # outter(index) # def index(x,y): # print(x,y) # 偷梁换柱之后 # index的参数什么样子,wrapper的参数就应该什么样子 # index的返回值什么样子,wrapper的返回值就应该什么样子 # index的属性什么样子,wrapper的属性就应该什么样子==》from functools import wraps # 山炮玩法: # def auth(func,db_type): # def wrapper(*args, **kwargs): # name=input('your name>>>: ').strip() # pwd=input('your password>>>: ').strip() # # if db_type == 'file': # print('基于文件的验证') # if name == 'egon' and pwd == '123': # res = func(*args, **kwargs) # return res # else: # print('user or password error') # elif db_type == 'mysql': # print('基于mysql的验证') # elif db_type == 'ldap': # print('基于ldap的验证') # else: # print('不支持该db_type') # # return wrapper # # # @auth # 账号密码的来源是文件 # def index(x,y): # print('index->>%s:%s' %(x,y)) # # # @auth # 账号密码的来源是数据库 # def home(name): # print('home->>%s' %name) # # # @auth # 账号密码的来源是ldap # def transfer(): # print('transfer') # # # index=auth(index,'file') # home=auth(home,'mysql') # transfer=auth(transfer,'ldap') # # # index(1,2) # # home('egon') # # transfer() # 山炮二 # def auth(db_type): # def deco(func): # def wrapper(*args, **kwargs): # name=input('your name>>>: ').strip() # pwd=input('your password>>>: ').strip() # # if db_type == 'file': # print('基于文件的验证') # if name == 'egon' and pwd == '123': # res = func(*args, **kwargs) # return res # else: # print('user or password error') # elif db_type == 'mysql': # print('基于mysql的验证') # elif db_type == 'ldap': # print('基于ldap的验证') # else: # print('不支持该db_type') # # return wrapper # return deco # # deco=auth(db_type='file') # @deco # 账号密码的来源是文件 # def index(x,y): # print('index->>%s:%s' %(x,y)) # # deco=auth(db_type='mysql') # @deco # 账号密码的来源是数据库 # def home(name): # print('home->>%s' %name) # # deco=auth(db_type='ldap') # @deco # 账号密码的来源是ldap # def transfer(): # print('transfer') # # # index(1,2) # home('egon') # transfer() # 语法糖 def auth(db_type): def deco(func): def wrapper(*args, **kwargs): name = input('your name>>>: ').strip() pwd = input('your password>>>: ').strip() if db_type == 'file': print('基于文件的验证') if name == 'egon' and pwd == '123': res = func(*args, **kwargs) # index(1,2) return res else: print('user or password error') elif db_type == 'mysql': print('基于mysql的验证') elif db_type == 'ldap': print('基于ldap的验证') else: print('不支持该db_type') return wrapper return deco @auth(db_type='file') # @deco # index=deco(index) # index=wrapper def index(x, y): print('index->>%s:%s' % (x, y)) @auth(db_type='mysql') # @deco # home=deco(home) # home=wrapper def home(name): print('home->>%s' % name) @auth(db_type='ldap') # 账号密码的来源是ldap def transfer(): print('transfer') # index(1, 2) # home('egon') # transfer() # 有参装饰器模板 def 有参装饰器(x,y,z): def outter(func): def wrapper(*args, **kwargs): res = func(*args, **kwargs) return res return wrapper return outter @有参装饰器(1,y=2,z=3) def 被装饰对象(): pass