一、引言
软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。
软件包含的所有功能的源代码以及调用方式,都应该避免修改。否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。
二、装饰器介绍
装饰代指为被装饰对象添加新的功能,器代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。
装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
提示:可调用对象有函数,方法或者类,此处我们单以本章主题函数为例,来介绍函数装饰器,并且被装饰的对象也是函数。
三、无参装饰器实现
函数装饰器分为:无参装饰器和有参装饰器两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物
1、无参装饰器的实现流程推导
接下来我们要实现一个装饰器,终极目标:它能计算任何函数的运行时间。
# 假如下面这个函数就是我们需要计算运行时间的函数 import time def index(): time.sleep(3) print("welcome to internet cafe !") index()
(1)在不改变函数体源代码和调用方式的前提下,我们能想到下面的办法
# version one start = time.time() index() end = time.time() print(end - start)
缺点:代码冗余很高,,而且看起来不简洁。因此我决定把这个抽成一个函数
(2)封装成函数,解决代码冗余
# version two def time_aculate(): start = time.time() res = index() #res先不用管为什么要return,后面的有参装饰器会解答。 end = time.time() print(end - start) return res time_aculate()
缺点:函数被写死,只能用于index函数运行时间的计算,且index函数的调用方式也发生了变化。
(3)把函数名写活
于是我们换一种为函数体传值的方式,即将值包给函数
# version three def timer(func): def time_aculate(): start = time.time() res = func() end = time.time() print(end - start) return res return time_aculate
这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能,只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数time_aculate重新赋值给变量名 /函数名index,如下
index = timer(index) #得到index = time_aculate,把index指向的原始内存地址改成了指向time_aculate内存地址,有人会说,那原始内存地址不就引用计数为0了吗?不会的,time_aculate携带对外作用域的引用:func = 传给timer函数的index(原始内存地址) index() # 执行的是time_aculate(),在time_aculate的函数体内再执行最原始的index #现在有个wrapper函数需要检测其执行时间: wrapper = timer(wrapper) wrapper()
(4)把参数、返回值写活
上面那种虽然已经很完美了。但是针对函数中参数个数经常发生变化这种需求,那么这个计算函数运行时间的装饰器就已经不能满足我们的需求了。我们应该把装饰器尽可能的独立开来,不受函数的变化的影响。
# version four def timer(func): def time_aculate(*args,**kwargs): start = time.time() res = func(*args,**kwargs) end = time.time() print(end - start) return res # 为什么还需要这个res呢?因为我们需要把time_aculate伪装成index函数,index有什么返回值,必须它也要返回什么值,结合最后两行代码理解。 return time_aculate
注意:上面这个timer就是一个成型的无参装饰器了,为任何函数可以添加计算其运行时间这个功能。
精髓透析:
把函数参数写活了
def time_aculate(*args,**kwargs): func(*args,**kwargs) # 这两行代码是最关键的,无论被测函数参数个数如何变,time_aculate都可以应对,且受func处,index原函数的参数个数和类型的制约。
把返回值写活了
res = func(*args,**kwargs) return res #没有这个 return res,如果被装饰器装饰的函数对象有返回值,那么你的装饰器就影响了原函数本身的功能。
2、无参装饰器实现总结
三个写活是无参装饰器的精髓所在:把函数名写活了、把函数参数写活了、把返回值写活了
3、语法糖
def timer(func): def time_aculate(*args,**kwargs): start = time.time() res = func(*args,**kwargs) end = time.time() print(end - start) return res return time_aculate index = timer(index) # 这行代码实现了time_aculate函数伪装成index函数 index()
如果你要调用装饰器,那么就肯定会有index = timer(index)这个伪装成原始函数的代码,假如我有很多地方都要用到这个装饰器,那么都要使用这句话,这样未免太过麻烦且很无趣,语法糖就是解决这个问题的。
import time def timer(func): def time_aculate(*args,**kwargs): start = time.time() res = func(*args,**kwargs) end = time.time() print(end - start) return res return time_aculate @timer # 这个就是语法糖,它把下面的index函数名存储的内存地址传给timer这个装饰器。相当于就是自动完成了index = timer(index)。 def index(): time.sleep(3) print("welcome to internet cafe !") index()
这样我们的代码会更简洁,更舒服。
4、一个函数叠加多个无参装饰器(即添加多个附加功能)
import time def login(time): #这里的time是在局部名称空间可以和 import time全局名称空间重复,且先搜索局部名称空间。 def check_user(*args,**kwargs): user = input("请输入您的用户名:").strip() pwd = input("请输入您的密码:").strip() if user == 'amy' and pwd == '123': res = time(*args,**kwargs) return res else: print('登录失败') return check_user def timer(func): def time_aculate(*args,**kwargs): start = time.time() res = func(*args,**kwargs) end = time.time() print('您的登录时间为:{}'.format(end - start)) return res return time_aculate @login # login后传参 @timer # timer先传参 def index(): time.sleep(3) print("welcome to internet cafe !") index()
执行结果:
请输入您的用户名:amy 请输入您的密码:123 welcome to internet cafe ! 您的登录时间为:3.0105090141296387 Process finished with exit code 0
上面就是叠加多个装饰器的使用方法。
流程详述:在写程序时,程序就存储在内存中。程序运行时,先是定义阶段,程序中所有函数的内存地址都已经有了。再是执行阶段,index函数执行,发现有装饰器存在,则先把index内存地址传参给最下面的timer装饰器函数,执行timer(index),返回一个time_aculate函数的内存地址,因为还有login装饰器,再把time_aculate的内存地址传给login装饰器函数,执行login(time_aculate),返回一个check_user的内存地址,然后 index = check_user的内存地址。再执行index()也即是check_user的内存地址(),然后一层层的执行即可。
装饰顺序:
index --> timer(func) --> login(time)
执行顺序:
check_user() --> time_aculate() -->index()
5、完美伪装原函数属性(了解即可)
(1)装饰器伪装不彻底问题
装饰器其实就是对被装饰函数添加功能后的一个偷梁换柱,伪装原函数。上面我们主要从三个层面进行伪装,函数名、返回值、函数参数。其实光这些层面的伪装依旧可以发现伪装后的index和index原函数不是一样的。但是装饰器的目的是达到了,功能已经添加了。
import time def login(time):... def timer(func):... #@login # login后传参 #@timer # timer先传参 def index(): '''这是一个网吧主页!''' time.sleep(3) print("welcome to internet cafe !") print(index.__name__) print(index.__doc__)
原始index函数的名字和注释文档属性信息
index 这是一个网吧主页!
取消语法糖的注释:
import time def login(time): #这里的time是在局部名称空间可以和 import time全局名称空间重复,且先搜索局部名称空间。 def check_user(*args,**kwargs): user = input("请输入您的用户名:").strip() pwd = input("请输入您的密码:").strip() if user == 'amy' and pwd == '123': res = time(*args,**kwargs) return res else: print('登录失败') return check_user def timer(func): def time_aculate(*args,**kwargs): start = time.time() res = func(*args,**kwargs) end = time.time() print('您的登录时间为:{}'.format(end - start)) return res return time_aculate @login # login后传参 @timer # timer先传参 def index(): '''这是一个网吧主页!''' time.sleep(3) print("welcome to internet cafe !") print(index.__name__) print(index.__doc__)
输出结果
check_user None
发现虽然我们在index的内存地址上偷梁换柱了,但相应的index函数的属性信息也发生了改变。这样我们伪装的还不够彻底。
(2)通过属性赋值的方法解决问题(效率低下)(经测试没有起作用,无需关注)
import time
def login(time):
def check_user(*args,**kwargs):
check_user.__name__ = func.__name # 伪装后的index,追根溯源存储的还是check_user函数的内存地址,因此我们改check_user函数属性即可
check_user.__doc__ = func.__doc__ # func中其实存储的是原始index函数的内存地址。
user = input("请输入您的用户名:").strip()
pwd = input("请输入您的密码:").strip()
if user == 'amy' and pwd == '123':
res = time(*args,**kwargs)
return res
else:
print('登录失败')
return check_user
def timer(func):
def time_aculate(*args,**kwargs):
start = time.time()
res = func(*args,**kwargs)
end = time.time()
print('您的登录时间为:{}'.format(end - start))
return res
return time_aculate
@login # login后传参
@timer # timer先传参
def index():
time.sleep(3)
print("welcome to internet cafe !")
print(index.__name__)
print(index.__doc__)
index伪装后,追根溯源还是check_user这个函数的内存地址,因此我们需要在login装饰器中修改check_user的函数属性。但是函数属性有很多很多,我们不可能一个个的赋值,达到两个函数属性一模一样。因此我们需要引进一个装饰器wraps,帮助我们去干这件事。
(3)一个装饰器使用wraps
from functools import wraps import time def timer(func): @wraps(func) #我们需要把check_user函数属性装饰成func的函数属性,因此在被装饰函数的上面写下语法糖,并在括号中指定你需要以那个函数的属性为模版进行装饰。 def time_aculate(*args,**kwargs): start = time.time() res = func(*args,**kwargs) end = time.time() print('您的登录时间为:{}'.format(end - start)) return res return time_aculate @timer def index(): '''文档注释''' time.sleep(3) print("welcome to internet cafe !") print(index.__name__) print(index.__doc__)
输出结果
index 文档注释
(4)叠加装饰器使用wraps
from functools import wraps import time def login(time): @wraps(time) def check_user(*args, **kwargs): user = input("请输入您的用户名:").strip() pwd = input("请输入您的密码:").strip() if user == '吴晋丞' and pwd == '123': res = time(*args, **kwargs) return res else: print('登录失败') return check_user def timer(func): @wraps(func) def time_aculate(*args, **kwargs): start = time.time() res = func(*args, **kwargs) end = time.time() print('您的登录时间为:{}'.format(end - start)) return res return time_aculate @login # login后传参 @timer # timer先传参 def index(): '''文档注释''' time.sleep(3) print("welcome to internet cafe !") print(index.__name__) print(index.__doc__)
运行结果
index 文档注释
四、有参装饰器的实现
@timer这个就是我们无参装饰器,@wraps(func)就是一个有参装饰器。
装饰器有无参数即是语法糖有无参数。
1、需求引入
from functools import wraps def vertificate(func): @wraps(func) def time_aculate(*args, **kwargs): a = {} with open('db.txt', mode='rb') as f: for line in f: res = line.decode(encoding='utf-8').strip('\n').strip('\r').split(':') a[res[0]] = res[1] count = 0 while count < 3: name = input('请输入您的用户名:').strip() pwd = input('请输入您的密码:').strip() if name in a and pwd == a[name]: print('登录成功,欢迎用户{}!'.format(name)) res = func(*args, **kwargs) return res else: count += 1 if count < 3: print('登录失败,账号或密码错误,请重新输入账号密码') else: print('对不起,您输入次数过多,请稍后再试!') return time_aculate @vertificate def index(): '''cmd命令窗口''' a = ('ls', 'pwd', 'tail', 'cd', 'll') while True: cmd = input('>>>').strip() if cmd == 'exit': break elif cmd in a: print('{}命令正在运行...'.format(cmd)) else: print('命令错误,请输入正确的命令!') continue index()
运行结果:
请输入您的用户名:ss 请输入您的密码:aa 登录失败,账号或密码错误,请重新输入账号密码 请输入您的用户名:amy 请输入您的密码:123 登录成功,欢迎用户amy! >>>ls ls命令正在运行... >>>dd 命令错误,请输入正确的命令! >>>dir 命令错误,请输入正确的命令! >>>tail tail命令正在运行... >>>exit Process finished with exit code 0
上面的装饰器部分单独拿下来看:
from functools import wraps def vertificate(func): @wraps(func) def time_aculate(*args, **kwargs): '''读取文件中的账号、密码信息''' '''核对输入账号密码,实现登录逻辑''' return time_aculate
账号和密码可以存在文件里,还有数据库、ldap验证都可以用来做账号、密码登录身份验证。
假如我们要让附加的验证功能可以自主选择验证方法,怎么实现?
答:自主选择那么一定需要传参。传进来file,则文件验证;传进来mysql,则为数据库验证;传进来ldap,则ldap验证。代码中再加if判断传进来的值即可。
2、初次想法(直接给装饰器函数多添加一个参数)
于是乎大家想的下面的代码便应运而生了:
from functools import wraps def vertificate(func, # check_method): @ wraps(func) def time_aculate(*args, **kwargs): '''读取文件中的账号、密码信息''' '''核对输入账号密码,实现登录逻辑''' return time_aculate
上述代码中check_method就是添加的参数,但是这样子没法传参。当然你如果不用语法糖,手工直接index = vertificate(index,mysql)这样也行,但是太low。
3、三层函数嵌套(值包给内部函数)
from functools import wraps def login(check_method): def vertificate(func): @wraps(func) def time_aculate(*args,**kwargs): '''读取文件中的账号、密码信息''' '''核对输入账号密码,实现登录逻辑''' return time_aculate return vertificate vertificate = login(mysql) # 改变vertificate内存地址,指向传参后的新的vertificate内存地址 @vertificate # 将index内存地址传给vertificate,它已经不是它了 def index():...
上面这个依旧不是最好的版本,语法糖依旧没有升级,请看下面这个终极版。
4、语法糖传参解决
注意,此传参和第二步中的初次设想中的传参意义完全不一样。要想理解第四步,必须好好看第三步
from functools import wraps def login(check_method): # 外面再嵌套了一层函数 def vertificate(func): @wraps(func) def time_aculate(*args,**kwargs): a = {} if check_method == 'file': print('正在使用文件验证...') with open('db.txt',mode='rb') as f: for line in f: res = line.decode(encoding='utf-8').strip('\n').strip('\r').split(':') a[res[0]] = res[1] elif check_method == 'mysql': print('正在使用mysql验证...') elif check_method == 'ldap': print('正在使用ldap验证...') else: print('不支持{}这种认证方式'.format(check_method)) count = 0 while count < 3: name = input('请输入您的用户名:').strip() pwd = input('请输入您的密码:').strip() if name in a and pwd == a[name]: print('登录成功,欢迎用户{}!'.format(name)) res = func(*args,**kwargs) return res else: count += 1 if count < 3: print('登录失败,账号或密码错误,请重新输入账号密码') else: print('对不起,您输入次数过多,请稍后再试!') return time_aculate return vertificate #且这里需要返回传参后的vertificate的内存地址 @login('file') #在这里控制验证的方式 def index(): '''cmd命令窗口''' a = ('ls', 'pwd', 'tail', 'cd', 'll') while True: cmd = input('>>>').strip() if cmd == 'exit': break elif cmd in a: print('{}命令正在运行...'.format(cmd)) else: print('命令错误,请输入正确的命令!') continue index() # 调用方式依旧没变。
执行结果:
正在使用文件验证... 请输入您的用户名:dd 请输入您的密码:aa 登录失败,账号或密码错误,请重新输入账号密码 请输入您的用户名:amy 请输入您的密码:123 登录成功,欢迎用户amy! >>>ls ls命令正在运行... >>>dd 命令错误,请输入正确的命令! >>>exit Process finished with exit code 0
等学了mysql验证、ldap验证,那么这些你都可以实现。
这里的语法糖传参和第二步中的初次设想的语法糖传参我必须说明一下区别:
首先语法糖的根本作用是把被装饰函数的内存地址作为参数传给装饰器,你装饰器直接再加一个参数,语法糖无法实现传参。
这里的语法糖传参,从第三步可以看出来,传入的参数只是执行了login('mysql') 将返回值給了语法糖作为被装饰函数传入参数的入口,语法糖做的事情依旧没有变的。
————————————————
版权声明:本文为CSDN博主「凤求凰的博客」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44571270/article/details/105972076