一、开放封闭原则(面向对象原则的核心)、装饰器作用
开放封闭原则:软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。
装饰器的作用:在不更改原功能函数内部代码,并且不改变调用方法的情况下为原代码添加新的功能。
二、装饰器原理阐述
答:将装饰器的函数当做一个参数传到装饰器中,并且让被装饰的函数名指向装饰器内部的函数,在装饰器的内部函数中用接收到的参数再调用被装饰的函数。
三、装饰器
(1)普通装饰器
第三讲的时候我们提到了闭包函数,decorator函数需要填写一个参数a,我们填写的11,
然后调用decorator(11)的时候,实际上就是返回的wrapper函数名称,然后将返回结果res加个()调用,实际上就是在调用wrapper函数,打印出来a=11
def decorator(a):
def wrapper():
print(a)
return wrapper
res = decorator(11)
print(res)
res()
# 打印结果
<function decorator.<locals>.wrapper at 0x000001AE6A16DC10>
11
那么我想问一下,这个decorator函数需要填写的参数a可以填写函数名吗?
答:当然可以,那有些小伙伴就要问你咋知道的?不要问,那是因为我试过。。哈哈,下面上代码。
我不仅能参数填写函数名,我在嵌套函数wrapper里面还能通过传入的函数名a,去调用这个函数a()。
def decorator(a):
def wrapper():
print('wrapper函数-----1')
print(a)
print('wrapper函数-----2')
a()
return wrapper
def work():
print('---------work函数--------')
# decorator(work) ===> 返回值是wrapper
# 这个时候res = wrapper
# 这个时候调用res(),实际上就是调用了wrapper()
# 那这个时候a是啥,a是你decorator传进去的参数,work方法名。所以打印结果为 <function work at 0x000001A404EACC10>
# a()实际上就是调用这个work函数
res = decorator(work)
res()
# 打印结果:
wrapper函数-----1
<function work at 0x000001BB392DDC10>
wrapper函数-----2
---------work函数--------
那么接下来,神奇的一幕要发生了,大家睁大眼睛。
我单纯的只是调用work_demo函数 他打印结果是不是work_demo函数里面执行的打印操作"工作函数:work_demo"。
def decorator(func):
def wrapper():
print('wrapper函数-----1')
func()
print('wrapper函数-----2')
return wrapper
def work_demo():
print("工作函数:work_demo")
work_demo()
# 打印结果
工作函数:work_demo
那这个时候我在work_demo函数上面加个装饰器@decorator,会是什么样的情况呢?
def decorator(func):
def wrapper():
print('wrapper函数-----1')
func()
print('wrapper函数-----2')
return wrapper
@decorator
def work_demo():
print("工作函数:work_demo")
work_demo()
# 打印结果
wrapper函数-----1
工作函数:work_demo
wrapper函数-----2
效果很明显,我在函数work_demo上添加装饰器@decorator之后,
- 先执行了print('wrapper函数-----1'),
- 而后再执行了func()也就是work_demo这个函数,
- 最后再print('wrapper函数-----2')
在一个函数(work_demo)上面加@ 函数名是代表什么意思呢?
答:相当于是将这个被@函数名的函数(work_demo),把这个函数名当成参数往decorator函数里面传,并且把返回值赋给work_demo, 所以就等同于 work_demo=decorator(work_demo)。
然后我们调用work_demo(),因为加了装饰器之后,work_demo=decorator(work_demo),decorator函数返回值又是嵌套函数wrapper,所以work_demo=wrapper,所以调用work_demo函数,等于调用了嵌套函数wrapper===> work_demo() ==>wrapper()
def decorator(func):
def wrapper():
print('wrapper函数-----1')
func()
print('wrapper函数-----2')
return wrapper
@decorator # work_demo=decorator(work_demo)
def work_demo():
print("工作函数:work_demo")
work_demo() # wrapper()
玩过自动化unittest的小伙伴都知道ddt数据驱动,他就是通过装饰器的原理来做的。我不加@data(11, 22, 33)他只能找到一条用例,我加了后他能找到三条用例。
案例:我需要一个装饰器,我在调用任何一个函数的时候,只要我加了这个装饰器,我就能知道调用这个函数执行的多长时间。
def decorator(func):
def wrapper():
start_time = time.time()
func()
end_time = time.time()
total_time = end_time - start_time
print('函数执行接收后的时间', total_time)
return wrapper
@decorator # work=decorator(work)
def work():
time.sleep(2)
print('原来函数的功能代码')
work()
# 打印结果
原来函数的功能代码
函数执行接收后的时间 2.0109193325042725
(2)装饰带参数的函数
从excel读取文件我封装了一个方法work1,一开始没有带filename和sheet参数,直接将filename和sheet写死,代码如下
def work1():
rows = openpyxl.load_workbook('data.xlsx')['login'].rows
title = [i.value for i in next(rows)]
return [dict(zip(title, [i.value for i in item])) for item in rows]
后面我做了一些改动,使得更加灵活一点,
- 将文件名和sheet名参数化,我随便传入文件名+sheet名,就能读取任意文件里面的内容。
- 引用装饰器,能够不修改原有work函数,就能使文件内容按case_id排序。
import openpyxl
def work2_sort(func):
def wrapper():
result = func()
result.sort(key=lambda x: x['case_id'])
return result
return wrapper
@work2_sort # work1 = work2_sort(work1)
def work1(filename, sheet):
rows = openpyxl.load_workbook(filename)[sheet].rows
title = [i.value for i in next(rows)]
return [dict(zip(title, [i.value for i in item])) for item in rows]
if __name__ == '__main__':
# 会报错
work1('data.xlsx', 'login')
# 打印结果
Traceback (most recent call last):
File "E:\pycharm\testing_and_development\py09_05day\demo1_装饰器装饰带参数的函数.py", line 27, in <module>
work1('data.xlsx', 'login')
TypeError: work2_sort.<locals>.wrapper() takes 0 positional arguments but 2 were given
但是当我们调用work1传入2个参数的时候会发现程序报错,为什么会报错?报错提示很明显,work1现在指向了wrapper函数,@work2_sort 等同于work1 = work2_sort(work1)。提示wrapper函数需要0个参数,但是你却给了2个参数。
那这个时候我们将wrapper函数传2个参数试试,反正他不是报错提示他需要0个参数我们却传了2个参数吗,那我们就给他写2个参数看看有没有问题,下面上代码。
import openpyxl
def work2_sort(func):
def wrapper(filename, sheet):
result = func()
result.sort(key=lambda x: x['case_id'])
return result
return wrapper
@work2_sort
def work1(filename, sheet):
rows = openpyxl.load_workbook(filename)[sheet].rows
title = [i.value for i in next(rows)]
return [dict(zip(title, [i.value for i in item])) for item in rows]
if __name__ == '__main__':
work1('data.xlsx', 'login')
# 打印结果
Traceback (most recent call last):
File "E:\pycharm\testing_and_development\py09_05day\demo1_装饰器装饰带参数的函数.py", line 27, in <module>
work1('data.xlsx', 'login')
File "E:\pycharm\testing_and_development\py09_05day\demo1_装饰器装饰带参数的函数.py", line 12, in wrapper
result = func()
TypeError: work1() missing 2 required positional arguments: 'filename' and 'sheet'
报错提示TypeError: work1() missing 2 required positional arguments: 'filename' and 'sheet',提示我们work1函数需要2个参数但是我们没有传,work1函数在哪里执行的?
那这个时候我们再调用func()函数的时候传入2个参数,看看效果。
哇,成功实现,是不是可以啦!!!那我们现在来总结一下,装饰器装饰带参数的函数的时候,我们需要做哪些事情?
- 装饰器装饰带参数的函数的时候,被装饰的函数需要传几个参数,那么在装饰器里的嵌套函数wrapper函数就要传对应几个参数。他需要几个,那嵌套函数就定义几个。
- 嵌套函数wrapper里面调用func()的时候,被装饰的函数需要传几个参数,那调用func()函数的时候也要传相应个数的参数。
(3)通用装饰器
那么问题又来了 , 如果被装饰的函数有时候需要传1个参数,有时候需要传2个参数,有时候不需要传参数,那这种情况该怎么处理?
答:如果同一个装饰器既要装饰有参数的函数,又要装饰无参数的函数,那么我们在传参的时候就设置成不定长参数,这样不管被装饰的函数有没有参数都能用。
def decorator(func):
def wrapper(*args, **kwargs):
print('----装饰器---start----')
func(*args, **kwargs)
print('----装饰器---end----')
return wrapper
@decorator
def work1():
print('----work1')
@decorator
def work2(a):
print('----work2----a:', a)
@decorator
def work3(a, b):
print('----work3----a---b--', a, b)
if __name__ == '__main__':
work1()
work2(111)
work3(111, 222)
# 打印结果
----work1
----work2----a: 111
----work3----a---b-- 111 222
那这个时候你们可爱的产品经理又给你提需求了:我work1 work2 work3函数有返回值咋办,我们先上代码试试,我们这个时候print(work1())发现他的返回值是None。
def decorator(func):
def wrapper(*args, **kwargs):
print('----装饰器---start----')
func(*args, **kwargs)
print('----装饰器---end----')
return wrapper
@decorator
def work1():
print('----work1')
return 'work1'
@decorator
def work2(a):
print('----work2----a:', a)
return 'work2'
@decorator
def work3(a, b):
print('----work3----a---b--', a, b)
return 'work3'
if __name__ == '__main__':
print(work1())
# 打印结果
----装饰器---start----
----work1
----装饰器---end----
None
为什么是None?因为我们在装饰器里的嵌套函数wrapper里面直接是调用func()函数,并没有将他保存下来并且return出去,那我们改下代码。发现打印结果由None变成work1
def decorator(func):
def wrapper(*args, **kwargs):
print('----装饰器---start----')
result = func(*args, **kwargs)
print('----装饰器---end----')
# 返回原功能函数work1的结果
return result
return wrapper
@decorator
def work1():
print('----work1')
return 'work1'
@decorator
def work2(a):
print('----work2----a:', a)
return 'work2'
@decorator
def work3(a, b):
print('----work3----a---b--', a, b)
return 'work3'
if __name__ == '__main__':
print(work1())
# 打印结果
----装饰器---start----
----work1
----装饰器---end----
work1
(4)装饰器去装饰类
我们试试用装饰器装饰函数的语法去装饰在类上面看看结果是啥样子的,上代码
我们发现,不管是装饰函数还是装饰类,只要在函数或者类上面写@decorator,那么他就等同于
TestDemo = decorator(TestDemo),之前是把函数名当成参数传到decorator里面,现在同样是把类名传到函数decorator里面。
def decorator(func):
def wrapper(*args, **kwargs):
print('----装饰器---start----')
result = func(*args, **kwargs)
print('----装饰器---end----')
return result
return wrapper
@decorator # TestDemo = decorator(TestDemo)
class TestDemo:
name = '自定义的类'
if __name__ == '__main__':
t1 = TestDemo()
print(t1.name)
# 打印结果:
----装饰器---start----
----装饰器---end----
自定义的类
我们看下当这个装饰器装饰完这个类之后,这个TestDemo指向的是什么:
指向的是wrapper这个嵌套函数
我们下面t1 = TestDemo()去创建对象时,此时TestDemo已经是指向wrapper了,所以TestDemo()实际上是去调用wrapper函数。里面的result = func(),此时func实际上是指向TestDemo,那func()实际上等同于TestDemo(),也就是创建对象,最后result接收,最后返回result,然后t1去接收。然后print(t1.name)打印,实际上就是实例对象调用类里面的name属性。
如果类需要被传参数,那该怎么传:
(5)带参数的装饰器
下面产品又提出新的需求了:假如我这个装饰器想要传参数进去,怎么处理?我们先试着在decorator后面加个参数看看运行结果:代码执行报错
def decorator(func):
def wrapper(*args, **kwargs):
print('-----装饰器执行前---start---')
result = func(*args, **kwargs)
print('-----装饰器执行后---end---')
return result
return wrapper
@decorator(30)
def work(a, b):
print('----func:-----', a + b)
if __name__ == '__main__':
work(11, 22)
# 打印结果
Traceback (most recent call last):
File "D:\测开\python高级编程\复习\py09_05day\demo4_装饰器的参数定义.py", line 19, in <module>
def work(a, b):
File "D:\测开\python高级编程\复习\py09_05day\demo4_装饰器的参数定义.py", line 11, in wrapper
result = func(*args, **kwargs)
TypeError: 'int' object is not callable
代码报错提示int对象不可被调用?所以为啥会报这个错呢。我们先看下装饰器传入参数后,代码的执行流程是啥样子的。
- 在功能函数work上面@decorator(30),此时decorator需要传的参数func此时是30。所以嵌套函数里面调用func()的时候,实际上就是30(),所以会报错,int对象不能被调用。
- work函数名被传到嵌套函数wrapper里,此时args: <function work at 0x0000001799FF951>
总结:如果我们需要在定义的装饰器里传入参数@decorator(30),很明显,我们定义的装饰器 2层已经不满足需求了,需要定义三层。
- 第一层:你定义的装饰器传几个参数,那么在你定义的装饰器中,最外层的函数就需要定义同等个数的参数。
- 第二层:用来接收你装饰的这个函数名称
- 第三层:用来接收你装饰的这个函数需要传的参数。
我们来看修改后的代码
def decorator(number): # 第一层接收 装饰器的参数
def wrapper1(func): # 第二层接收 被装饰的函数名称
def wrapper(*args, **kwargs): # 第三层接收 被装饰的函数的参数
print('装饰器传的参数为:', number)
print('-----装饰器执行前---start---')
result = func(*args, **kwargs)
print('-----装饰器执行后---end---')
# 返回功能函数的调用的结果
return result
return wrapper
return wrapper1
@decorator(30)
def work(a, b):
print('----func:-----', a + b)
if __name__ == '__main__':
work(11, 22)
# 打印结果
装饰器传的参数为: 30
-----装饰器执行前---start---
----func:----- 33
-----装饰器执行后---end---
代码实现逻辑以及代码执行的流程:
-
先执行 decorator(30) ,此时调用decorator函数,number=30,返回wrapper1
-
此时 @decorator(30) ==》》 @wrapper1
-
@wrapper1 装饰work函数 等同于==》》 work = wrapper1(work)
-
此时 wrapper1函数需要传的参数func 等同于 work,并且 代码中“return wrapper”,返回结果wrapper由 work接收
-
然后 第三层嵌套函数 def wrapper(*args, **kwargs) 等同于 def work(*args, **kwargs),我们调work(11,22)函数的时候,work需要传2个参数11, 22,此时就是在调用wrapper函数。
注意:1-3步骤,实际上就是:
被装饰的函数名 = 装饰器(装饰器参数)(被装饰的函数名)
work = decorator(30)(work)
(6)类实现装饰器(魔术方法中会讲)
(7)多个装饰器装饰同一个函数的运行流程
当我们只在功能函数上添加一个装饰器,我们查看执行结果:
def wang1(func):
def wrapper(*args, **kwargs):
print('-----装饰器---wang1---start---')
result = func(*args, **kwargs)
print('-----装饰器----wang1---end---')
return result
return wrapper
def wang2(func):
def wrapper(*args, **kwargs):
print('-----装饰器---wang2---start---')
result = func(*args, **kwargs)
print('-----装饰器---wang2---end---')
return result
return wrapper
@wang2
def work(a, b):
print('----dunc----:', a + b)
if __name__ == '__main__':
work(11, 22)
# 打印结果
-----装饰器---wang2---start---
----dunc----: 33
-----装饰器---wang2---end---
这个时候我们再添加一个装饰器@wang1
def wang1(func):
def wrapper(*args, **kwargs):
print('-----装饰器---wang1---start---')
result = func(*args, **kwargs)
print('-----装饰器----wang1---end---')
return result
return wrapper
def wang2(func):
def wrapper(*args, **kwargs):
print('-----装饰器---wang2---start---')
result = func(*args, **kwargs)
print('-----装饰器---wang2---end---')
return result
return wrapper
@wang1
@wang2
def work(a, b):
print('----dunc----:', a + b)
if __name__ == '__main__':
work(11, 22)
# 打印结果
-----装饰器---wang1---start---
-----装饰器---wang2---start---
----dunc----: 33
-----装饰器---wang2---end---
-----装饰器----wang1---end---
看到打印结果,大家也猜到代码执行逻辑是什么样子的了:
答: 就近原则,如果存在多个装饰器装饰同一个函数,那就选离功能函数work最近的一个装饰器先装饰,@wang2 >>> wang2(work), 然后上面又有一个装饰器@wang1,此时将wang2装饰器已经装饰完的结果再去用wang1装饰器去装饰, wang1(wang2(work)),最终把这个装饰完的结果 给这个功能函数work去接收。 等同于>>> work = wang1(wang2(work))
- wang2(work) ,实际上就是返回装饰器wang2里面的wrapper函数名
- 此时 wang1(wang2(work)), 装饰器wang1需要传入的参数func其实就是wang2(work),月就是wang2里面的wrapper函数名
- 此时work = wang1(wang2(work)),此时调用work1函数,因为 wang1(wang2(work)) 返回值是wang1装饰器里面的wrapper函数名,所以调用work1函数,实际上是调用装饰器wang1里面的wrapper函数,func等于 wang1(wang2(work)),也就是装饰器wang2里面的wrapper函数。
- 装饰器wang2里面的func指向的是什么?指向的是原功能函数。
总结:
1、就近原则,靠近原功能函数的装饰器先执行,离功能函数相对比较远的晚执行。
2、离功能函数相对较远的装饰器,他传的func参数实际上就是离功能函数相对近的装饰器里面的嵌套函数wrapper。
3、离功能函数相对较近的装饰器,需要传的func参数实际上就是指向原功能函数work名。
(8)装饰器的其他定义形式以及他对应的场景
①、闭包形式的装饰器:一般用来给原功能函数调用之前或者之后做功能扩展(上面就是闭包形式的装饰器)
②、类实现装饰器: 一般用来给原功能函数调用之前或者之后做功能扩展(后面讲魔法函数的时候会讲)
③、普通函数作为装饰器: 一般是用来对被装饰的函数(类)的属性进行修改。
下面上Demo,此时我们打印work看他的文档注释,发现打印结果是None。
这个时候我们在装饰器中添加一行代码试试,此时打印结果有值
func.__doc__ = '装饰器新增的文档字符串注释'
还有 一点需要注意的是,这行代码:func.__doc__ = '装饰器新增的文档字符串注释'是在没有调用功能函数的时候就执行了。一般是用来对他的属性进行相关的修改。如果装饰的是类,就可以动态的对这个类的属性进行添加。
(9)内置装饰器
① functools.wraps: (消除装饰器的副作用)
Demo:我先不加装饰器,也不加wraps,我们看打印结果,没毛病。
def decorator(func):
def wrapper():
func()
return wrapper
def work01():
"""这个是work01"""
print("-----work01------")
if __name__ == '__main__':
work01()
# 获取work01的函数名称
print(work01.__name__)
# 获取work01的文档字符串注释
print(work01.__doc__)
# 打印结果
-----work01------
work01
这个是work01
我们这个时候把装饰器添加上去试试看打印结果:因为我们加了装饰器之后,我们调用work01(),实际上是调用的wrapper函数,所以打印work01的函数名称name的时候,显示的是wrapper(闭包函数的名字),然后文档字符串注释也没有。
def decorator(func):
def wrapper():
func()
return wrapper
@decorator # work01 = decorator(work01)
def work01():
"""这个是work01"""
print("-----work01------")
if __name__ == '__main__':
work01()
# 获取work01的函数名称
print(work01.__name__)
# 获取work01的文档字符串注释
print(work01.__doc__)
# 打印结果:
-----work01------
wrapper
None
那这个时候我们吧wraps内置装饰器加上去试试看:效果显而易见,这个wraps内置装饰器的作用是什么!!
答:我希望我打印函数名称和文档字符串的时候,我希望还是那个被装饰器装饰的函数名称,而不晒闭包函数的名称的时候,就需要用到wraps内置装饰器,消除装饰器装饰之后的一个副作用的。
def decorator(func):
@wraps(func)
def wrapper():
func()
return wrapper
@decorator # work01 = decorator(work01)
def work01():
"""这个是work01"""
print("-----work01------")
if __name__ == '__main__':
work01()
# 获取work01的函数名称
print(work01.__name__)
# 获取work01的文档字符串注释
print(work01.__doc__)
# 打印结果
-----work01------
work01
这个是work01
② functools.lru_cache:(缓存装饰器)
四、实战练习题
"""
1、现有有如下功能函数:
def work(a,b):
res = a/b
print('a除B的结果为:',res)
# 调用函数当参数b为0的时候,work执行会报错!如:work(10,0)会报错
# 需求:在不更改函数代码的前提现,实现调用work函数传入参数b为0时,函数不报错,输出结果:参数b不能为零
2、实现一个重运行的装饰器,可以用来装饰任何一个功能函数,只要被装饰的函数执行出现AssertionError,
则重新执行该函数,同一个函数最多重运行三次。
3、编写一个鉴权装饰器,调用被装饰的函数时,需要校验用户是否登录,如果没有登录则需要输入账号,
输入密码进行登录(默认正确的账号lemonban,密码123456),
要求:只要有一个函数调用时登录成功,后续的函数都无需再输入用户名和密码
思路提示:
1、设置一个全局变量来保存是否登录的状态,
2、在装饰器中通过判断状态来决定是否要输入账号密码进行登录(登录成功之后修改登录状态)
4、(面试笔试题)请设计一个装饰器,接收一个int类型的参数number,可以用来装饰任何的函数,
如果函数运行的时间大于number,则打印出函数名和函数的运行时间
"""
import time
# 第一题:
def decorator1(func):
def wrapper(a, b):
if b == 0:
print('参数b不能为零')
else:
return func(a, b)
return wrapper
# 第二题
def rerun(func):
def wrapper(*args, **kwargs):
for i in range(4):
try:
if i == 0:
print("开始执行功能函数")
else:
print('进行第--{}---次重运行'.format(i))
res = func(*args, **kwargs)
except AssertionError as e:
print("运行失败!")
if i == 3:
raise e
else:
print('执行通过')
return res
return wrapper
# 第三题
status = {"status": False}
# 登录权限校验装饰器
def login_check(func):
def wrapper(*args, **kwargs):
# 判断登录状态是否为True
if status['status']:
func(*args, **kwargs)
else:
print('--没有登录不能调用{},请先登录'.format(func))
username = input('账号:')
password = input('密码:')
# 登录校验
if "lemonban" == username and "123456" == password:
status['status'] = True # 修改登录状态值
func(*args, **kwargs)
else:
print('账号或者密码有误,无权限调用该函数')
return wrapper
# 第四题
# 计算函数运行时间的装饰器
def count_time(number):
def decor(func):
def wrapper(*args, **kwargs):
s_time = time.time()
res = func(*args, **kwargs)
e_time = time.time()
if (e_time - s_time) > number:
print("函数名:{},运行时间:{}".format(func, e_time - s_time))
return res
return wrapper
return decor
# --------------------1----------------------------
@decorator1
def work(a, b):
res = a / b
print('a除B的结果为:', res)
# --------------------2----------------------------
@rerun
def demo1_test():
expected = 'abc'
res = input('请输入实际结果:')
assert expected == res
# --------------------3-----------------------------
@login_check
def index():
print('这个是index首页')
@login_check
def page1():
print('这个是page1页面')
@login_check
def page2():
print('这个是page2页面')
# --------------------4-----------------------------
@count_time(3)
def func_1():
for i in range(3):
time.sleep(1)
if __name__ == '__main__':
# demo1_test()
index()
page1()
page2()
# func_1()