装饰器
装饰器
一、装饰器简介
为何要用装饰器
软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其进行修改。
软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。
什么是装饰器
’装饰’代指为被装饰对象添加新的功能,’器’代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
提示:可调用对象有函数,方法或者类,此处我们单以本章主题函数为例,来介绍函数装饰器,并且被装饰的对象也是函数。
本质
虽然装饰器功能强大并且理解比较困难,但是他并不是一个新的知识点,而是由函数参数、名称空间、函数名多种用法、闭包函数组合到一起的结果。
口诀
对修改封闭 对扩展开放
储备知识
由于接下来的装饰器推到流程中,需要用到时间模块来计时,这就需要介绍一下时间模块的一些功能:
import time
# 表示引入时间模块
print(time.time()) # 返回时间戳(距离1970-01-01 00:00:00所经历的秒数)
time.sleep(3) # 这是让程序在运行到这里的时候暂停三秒的作用
print('睡醒了 干饭')
count = 0
# 循环之前先获取时间戳
start_time = time.time()
while count < 100:
print('哈哈哈')
count += 1
end_time = time.time()
# 循环解释后获取时间戳
print('循环消耗的时间:', end_time - start_time)
二、装饰期推导流程
这里我们定义两个函数,并想办法使用装饰器来达到统计调用函数运行时间的目的,修改代码期间需要时刻牢记装饰器的作用:在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能
步骤一:
先写出直接达成功能的代码
import time
# 引入时间模块
def index():
time.sleep(3)
print('from index')
def home():
time.sleep(1)
print('from home')
# 定义两个会暂停一段时间的函数,方便等下统计运行时间
'''1.当我们刚解除这个要求的时候,会直接采取最直接的方法看看完成功能的代码的基本情况'''
start_time = time.time()
index()
end_time = time.time()
print('函数index的执行时间为>>>:', end_time-start_time)
步骤二:
使用函数简化代码
'''2.我们发现针对不同的函数需要执行类似的一段代码,这个时候我们应该想到函数这个功能来简化代码'''
def get_time():
start_time = time.time()
index()
end_time = time.time()
print('函数index的执行时间为>>>:', end_time - start_time)
get_time()
步骤三:
尝试用参数传参
'''3.由于我们这个函数是要适用于不同的函数统计运行的时间的,所以需要给函数定义参数,并把内部代码中的具体函数名换成参数名加括号'''
def get_time(xxx):
start_time = time.time()
xxx()
end_time = time.time()
print('函数的执行时间为>>>:', end_time - start_time)
get_time(index)
get_time(home)
步骤四:
尝试用闭包函数传参
'''4.由于加括号的形式改变了调用阶段的调用方式,需要新的参数,所以我们想到了第二种给函数传参的方式——闭包函数'''
# 闭包函数的特点:嵌套函数和内部函数调用外部函数的名称
def outer(xxx):
# xxx = index
def get_time():
start_time = time.time()
xxx()
end_time = time.time()
print('函数的执行时间为>>>:', end_time - start_time)
return get_time
res = outer(index)
res()
res1 = outer(home)
res1()
步骤五:
通过改变调用变量名的名称为被计时函数的名称来达成不改变调用方式的目的
'''5.改动到这里后我们发现我们的调用方式还是不对,因此我们想到不如用被检测的那个函数的名称来当变量名,这样偷天换日之后就等于没有改变调用方式'''
def outer(xxx):
def get_time():
start_time = time.time()
xxx()
end_time = time.time()
print('函数的执行时间为>>>:', end_time - start_time)
return get_time
res = outer(index) # 赋值符号的左边是一个变量名 可以随意命名
res1 = outer(index)
res2 = outer(index)
jason = outer(index)
index = outer(index)
index()
home = outer(home)
home()
步骤六:
尝试给函数加上参数
'''6.但是我们在运行的时候又发现这个时候的半成品装饰器不能接收参数,于是我们给他加上参数(内部用于统计运行时间的函数)'''
def func(a):
time.sleep(0.1)
print('from func', a)
def func1(a,b):
time.sleep(0.2)
print('from func1', a, b)
def func2():
time.sleep(0.3)
print('from func2')
func(123)
def outer(xxx):
def get_time(a, b):
start_time = time.time()
xxx(a, b)
end_time = time.time()
print('函数的执行时间为>>>:', end_time - start_time)
return get_time
func1 = outer(func1)
func1(1, 2)
func = outer(func)
func(1)
func2 = outer(func2)
func2()
步骤七:
使用可变长参数来接收和传递参数
'''7.一些情况下我们又发现有些函数有参数有些函数没参数,因此我们想到使用可变长参数来达成接收这些参数并给计时的函数传参的方式'''
def func(a):
time.sleep(0.1)
print('from func', a)
def func1(a,b):
time.sleep(0.2)
print('from func1', a, b)
def outer(xxx):
# 这里的*args, **kwargs是定义阶段的形参——可变长形参,用于接收数据值
def get_time(*args, **kwargs): # get_time(1,2,3) args=(1,2,3)
start_time = time.time()
# 这里的*args, **kwargs是调用阶段的实参——可变长实参,把之前接收的数据值一个个拆分出来传参给被计时的函数
xxx(*args, **kwargs) # xxx(*(1,2,3)) xxx(1,2,3)
end_time = time.time()
print('函数的执行时间为>>>:', end_time - start_time)
return get_time
func = outer(func)
func(123)
func1 = outer(func1)
func1(1, 2)
步骤八:
给函数加上返回值
'''8.处理完这些之后我们又考虑到函数返回参数的话没有变量名接收这个返回值,于是我们在检测运行时间的时候给内部调用函数的代码绑定上变量名,之后在返回这个变量名'''
def func(a):
time.sleep(0.1)
print('from func', a)
return 'func'
def func1(a,b):
time.sleep(0.2)
print('from func1', a, b)
return 'func1'
def outer(xxx):
def get_time(*args, **kwargs):
start_time = time.time()
res = xxx(*args, **kwargs)
end_time = time.time()
print('函数的执行时间为>>>:', end_time - start_time)
return res
return get_time
# func = outer(func)
# res = func(123)
# print(res)
func1 = outer(func1)
res = func1(123, 123)
print(res)
三、装饰器模版
如果实在不能理解上文中的推导流程就直接记忆这个模版,模版中给的变量名函数名都是可以替换的
# 务必掌握
def outer(func):
def inner(*args, **kwargs):
# 执行被装饰对象之前可以做的额外操作
res = func(*args, **kwargs)
# 执行被装饰对象之后可以做的额外操作
return res
return inner
四、装饰器语法糖
当我们使用上面的装饰器模版的时候,改变调用方式的那个、偷天换日的操作容易造成误导。因此我们引进语法糖的概念:
def outer(func_name):
def inner(*args, **kwargs):
print('执行被装饰对象之前可以做的额外操作')
res = func_name(*args, **kwargs)
print('执行被装饰对象之后可以做的额外操作')
return res
return inner
"""
语法糖会自动将下面紧挨着的函数名当做第一个参数自动传给@函数调用,语法糖就是上文中把装饰器调用名变成原函数的操作
"""
@outer # func = outer(func)
def func():
print('from func')
return 'func'
@outer # index = outer(index)
def index():
print('from index')
return 'index'
func()
index()
五、多层语法糖
多层语法糖实际应用中出现较少,但是我们也需要了解相关的运行原理:
1、先判断距离被语法糖作用的函数最近的语法糖,它的作用就是把被装饰函数放入装饰器的函数中当参数。这个时候装饰器外层的函数会返回内部函数名。
2、接下来我们判断第二个装饰器的语法糖。这个语法糖的作用也是把下方得到的函数传到第二个装饰器中去,由于最下方那个语法糖的作用我们得到了最下方那个语法糖对应的内层函数名,所以这里就是把这个内层的函数名传到第二个装饰器中,然后我们会得到第二个装饰器返回给我们它的内层函数名。
3、最上方的语法糖的原理跟第二个类似,把第二个装饰器返回的内层函数名当成参数传到最上方语法糖对应的装饰器中,这里我们会得到最上方语法糖对应的装饰器内层的函数名称,并将她和被装饰函数的名称绑定。
'''注意语法糖会将下面紧挨着的函数当作参数传递给@符号后面的函数名运行'''
def outter1(func1): # 12.这个时候func1就接到了wrapper2的函数名了
print('加载了outter1') # 13.打印加载outter1
def wrapper1(*args, **kwargs): # 14.在创建新的wapper1 return
print('执行了wrapper1') # 16.打印wrapper1
res1 = func1(*args, **kwargs) # 17.遇到括号优先执行 这个时候的fun1是wrapper2所以直接跳到wrapper2
return res1
return wrapper1 # 15. 返回 wrapper1
def outter2(func2): # 07.所以这个时候的func2就是wrapper3
print('加载了outter2') # 08.打印加载outter2
def wrapper2(*args, **kwargs): # 09.然后定义了wrapper2 下一步return
print('执行了wrapper2') # 18.打印wrapper2
res2 = func2(*args, **kwargs) # 19.func2就是wrapper1
return res2
return wrapper2 # 10.这个时候返回wrapper2就是index返回了
def outter3(func3): # 02.这个时候fun3是真的index 运行fun3函数
print('加载了outter3') # 03.打印加载了outter3 函数定义不用看代码函数体代码 所以下一步 return
def wrapper3(*args, **kwargs):
print('执行了wrapper3') # 20.打印wrapper3
res3 = func3(*args, **kwargs) # 21.func3就是index跳到index
return res3
return wrapper3 # 04.所以这时候index的返回值就是wrapper3 这个时候wrapper就成了函数名 再次触发 outter2
@outter1 #11.这个时候index就到了outter1在运行outter1 15.这个时候就变成伪装的 index = routter1(wrapper2)
@outter2 # 06.所以这个时候wrapper3会传到outter2里面在运行
@outter3 # 01.这个时候就会把真正的index传给outter3 所以 outer3(index) 括号优先级执行 05.所以这个时候就是 wrapper3 = outter3(index)
def index():
print('from index') # 22.结束
index()
"""
多层语法糖 加载顺序由下往上
每次执行之后如果上面还有语法糖 则直接将返回值函数名传给上面的语法糖
如果上面没有语法糖了 则变形 index = outter1(wrapper2)
"""
六、有参装饰器
当我们使用装饰器的时候,如果装饰器内部的代码需要参数的时候我们发现不能用传参的方式来添加参数了,所以我们又使用闭包函数在外层包了一个函数,打到传参的目的。
注:当我们看到语法糖后面跟着括号和参数的时候,需要先看函数名和括号内的内容,然后再应用语法糖功能,在我们运行之后会发现有参装饰器又变回了之前的普通装饰器只是多了一个局部名称空间读取变量。
# 校验用户是否登录装饰器
def outer(mode):
def login_auth(func_name):
# 这里我们可以看出来是用来传函数名的,不适合加参数了
def inner(*args, **kwargs):
# 这里我们又可以看出来所有的参数都是加到内部被装饰函数中的,也不适合传别的参数所以就使用了闭包函数传参
username = input('username>>>:').strip()
password = input('password>>>:').strip()
if mode == '1':
print('数据直接写死')
elif mode == '2':
print('数据来源于文本文件')
elif mode == '3':
print('数据来源于字典')
elif mode == '4':
print('数据来源于MySQL')
return inner
return login_auth
'''当装饰器中需要额外的参数时>>>:有参装饰器'''
"""
函数名加括号执行优先级最高 有参装饰器的情况
先看函数名加括号的执行
然后再是语法糖的操作
"""
@outer('1')
def index():
print('from index')
index()
@outer('2')
def func():
print('from func')
func()
# 如果我们不想使用装饰器功能了,就把语法糖部分的代码注释掉就可以正常运行了。
七、有参装饰器模板
def outter(add_n):
def middle(func):
def inner(*args, **kwargs):
# 这里写一些代码需要参数的那种,把最外层的add_n用进来就可以了
res = func(*args, **kwargs)
return res
return inner
return middle
@outter('参数')
def index():
pass
无参装饰器模板昨天写了
八、装饰器修复技术
装饰器修复技术不算重点知识,但是在面试的时候可以装逼。
这里需要先介绍一下help方法的作用:用于查看函数或模块用途的详细说明。
如果一些小白使用help方法查看我们被装饰函数的用法,会发现查看到的是装饰器函数的内容,这时候我们使用一些小手段就可以让help方法指向的内容回到原函数:
1、在装饰器函数上面加上from functools import wraps
2、并且在内层函数前加上@wraps(func_name),这里括号内写装饰器外层的参数名称就可以了。
def index():
"""index函数 非常的牛"""
pass
help(index)
help(len)
# 这是使用修复技术前的代码,返回的结果会是外层装饰器的内容
from functools import wraps
def outer(func_name):
@wraps(func_name) # 仅仅是为了让装饰器的效果更加逼真 平时可以不写
def inner(*args, **kwargs):
"""我是inner 我擅长让人蒙蔽"""
res = func_name(*args, **kwargs)
return res
return inner
@outer
def func():
"""我是真正的func 我很强大 我很牛 我很聪明"""
pass
# help(func)
# print(func)
func()
# 这个时候我们发现代码运行后返回的是被装饰函数原本的内容