【python进阶 笔记】装饰器 (重点)
目录
1. 装饰器介绍
装饰器是程序开发中经常会用到的一个功能,用好了装饰器,开发效率如虎添翼,这也是Python面试必备的问题。
1.1. 引入代码
#### 第一段 ####
def foo():
print('foo')
foo # 表示是函数,变量名指向了函数
foo() # 表示执行foo函数
#### 第二段 ####
def foo():
print('foo')
foo = lambda x: x + 1 # lambda 匿名函数,默认返回值是表达式的结果
foo() # 执行lambda表达式,而不再是原来的foo函数,因为foo这个名字被重新指向了另外一个匿名函数
在python中,所有的方法名、函数名、变量名和类的名字通常是不允许相同的,python中都把他们当作一个变量名来对待。
故上面的代码中,函数名仅仅是个变量,只不过指向了定义的函数而已,所以才能通过 函数名()调用。如果 函数名=xxx被修改了,那么当在执行 函数名()时,调用的就不是之前的那个函数了。
1.2. 需求来了(通过需求逐步了解)
初创公司有N个业务部门,基础平台部门负责提供底层的功能,如:数据库操作、redis调用、监控API等功能。业务部门使用基础功能时,只需调用基础平台提供的功能即可。如下:
############### 基础平台提供的功能如下 ###############
def f1():
print('f1')
def f2():
print('f2')
def f3():
print('f3')
def f4():
print('f4')
############### 业务部门A 调用基础平台提供的功能 ###############
f1()
f2()
f3()
f4()
############### 业务部门B 调用基础平台提供的功能 ###############
f1()
f2()
f3()
f4()
目前公司有条不紊的进行着,但是,以前基础平台的开发人员在写代码时候没有关注验证相关的问题,即:基础平台的提供的功能可以被任何人使用。现在需要对基础平台的所有功能进行重构,为平台提供的所有功能添加验证机制,即:执行功能前,先进行验证。
(方案1)老大把工作交给 Low B,他是这么做的:
跟每个业务部门交涉,每个业务部门自己写代码,调用基础平台的功能之前先验证。诶,这样一来基础平台就不需要做任何修改了。太棒了,有充足的时间泡妹子...
当天Low B 被开除了…
(方案2:对相关功能的函数进行一一修改)老大把工作交给 Low BB,他是这么做的:
############### 基础平台提供的功能如下 ###############
def f1():
# 验证1
# 验证2
# 验证3
print('f1')
def f2():
# 验证1
# 验证2
# 验证3
print('f2')
def f3():
# 验证1
# 验证2
# 验证3
print('f3')
def f4():
# 验证1
# 验证2
# 验证3
print('f4')
############### 业务部门不变 ###############
### 业务部门A 调用基础平台提供的功能###
f1()
f2()
f3()
f4()
### 业务部门B 调用基础平台提供的功能 ###
f1()
f2()
f3()
f4()
(代码重用性太大不行)过了一周 Low BB 被开除了…
(方案3:定义一个函数用来调用)老大把工作交给 Low BBB,他是这么做的:
只对基础平台的代码进行重构,其他业务部门无需做任何修改
############### 基础平台提供的功能如下 ###############
def check_login():
# 验证1
# 验证2
# 验证3
pass
def f1():
check_login()
print('f1')
def f2():
check_login()
print('f2')
def f3():
check_login()
print('f3')
def f4():
check_login()
print('f4')
老大看了下Low BBB 的实现,嘴角漏出了一丝的欣慰的笑,语重心长的跟Low BBB聊了个天:
老大说:
写代码要遵循 开放封闭 原则,虽然在这个原则是用的面向对象开发,但是也适用于函数式编程,简单来说,它规定已经实现的功能代码不允许被修改,但可以被扩展,即:
- 封闭:已实现的功能代码块,不能再进行修改
- 开放:对扩展开发
如果将开放封闭原则应用在上述需求中,那么就不允许在函数 f1 、f2、f3、f4的内部进行修改代码,老板就给了Low BBB一个实现方案:
(方案4:用装饰器)
def w1(func):
def inner():
# 验证1
# 验证2
# 验证3
func()
return inner
@w1
def f1():
print('f1')
@w1
def f2():
print('f2')
@w1
def f3():
print('f3')
@w1
def f4():
print('f4')
对于上述代码,也是仅仅对基础平台的代码进行修改,就可以实现在其他人调用函数 f1 f2 f3 f4 之前都进行【验证】操作,并且其他业务部门无需做任何操作。
这段代码的内部执行原理:
以下方代码为例:
def set_func(func):
def call_func():
print("---这是权限验证1----")
print("---这是权限验证2----")
func()
return call_func
@set_func
def test1():
print("-----test1----")
test1()
#输出:
#---这是权限验证1----
#---这是权限验证2----
#-----test1----
- 若需要修改 函数A 的功能(如:在函数调用前要加一部分代码),可以在函数A的外面定义一个闭包,在函数A的前面写上 @闭包外部函数名 ,这样就会先执行 @的函数 。就可以实现在原来函数整体不修改的前提下增加功能。
- 如上,在test1函数前添加 @set_func 就可以实现添加“验证” 。
1.3. 装饰器的实现过程(重要)
def set_func(func):
def call_func():
print("---这是权限验证1----")
print("---这是权限验证2----")
func()
return call_func
# 代码段1----------
@set_func # 等价于test1 = set_func(test1)
def test1():
print("-----test1----")
test1() # 可通俗的看做调用了call_func
# 代码段2------------
# def test1():
# print("-----test1----")
# ret = set_func(test1)
# ret()
# 代码段3-----------
# def test1():
# print("-----test1----")
# test1 = set_func(test1)
# test1()
分别运行代码段1、2、3的输出结果一样:
---这是权限验证1----
---这是权限验证2----
-----test1----
分析:
- 先看 代码段2:通过闭包实现了添加功能 添加了两句输出。
- 再看 代码段1:在函数 test1定义前加上 @set_func , 等价于test1 = set_func(test1) 。此处使用了装饰器,整个代码段1与 代码段3完全等价。
- 代码段3:
- 在python中,所有的方法名、函数名、变量名和类的名字通常是不允许相同的。
- 但代码段3中,在 函数test1 定义后,语句 test1 = set_func(test1) 又定义了变量test1 用来指向set_func(test1) 的返回结果;
- set_func(test1)将函数test1 的引用作参数传给了函数set_func,set_func内定义 函数call_func 并作函数返回值 (此时call_func中的func 指向原来的函数test1,func() 即调用函数test1) ;
- 此时 set_func(test1) 的返回值指向了 函数call_func,(包括添加的功能代码和原来的test1函数代码),再定义 变量test1 来接收其返回值,这是,可以通俗的说执行test1()就会调用call_func。
- 这样,巧妙的实现了给 函数test1 添加功能,又用变量test1重新指向添加功能后的代码。相比代码段1,调用时仍然使用 test1 来调用。
- 代码段3 即代码段2中使用 装饰器的实现过程。
代码段2装饰器和代码段3中的优点:
- 不用修改原函数内部的代码可以加功能;
- 调用时依然用原来的函数名来调用,无需修改。
- 即能做到调函数不变的情况下,做到功能的拓展。
注意,装饰器只能在函数的前或者后添加代码或者说功能。
2. 装饰器(decorator)作用
- 引入日志
- 函数执行时间统计
- 执行函数前预备处理
- 执行函数后清理功能
- 权限校验等场景
- 缓存
demo:统计函数的运行时间
import time
def set_func(func):
def call_func():
start_time = time.time()
func()
stop_time = time.time()
print("alltimeis %f" % (stop_time - start_time))
return call_func
@set_func # 等价于test1 = set_func(test1)
def test1():
print("-----test1----")
for i in range(10000):
pass
test1()
3. 装饰器示例
3.1. 对无返回值、无参数函数的装饰
参考1.3的demo即可。
3.2. 对有参数、无返回值的函数进行装饰
def set_func(func):
def call_func(a):
print("---这是权限验证1----")
print("---这是权限验证2----")
func(a)
return call_func
@set_func # 相当于 test1 = set_func(test1)
def test1(num):
print("-----test1----%d" % num)
test1(100) # 可通俗的看做调用了call_func
# test1(200)
test1(100)可通俗的看作是调用了call_func,100传给a,a再传给func,func的实参即原来的函数test1。
3.3. 用同一个装饰器对多个函数进行装饰
def set_func(func):
def call_func(a):
print("---这是权限验证1----")
print("---这是权限验证2----")
func(a)
return call_func
@set_func # 相当于 test1 = set_func(test1)
def test1(num):
print("-----test1----%d" % num)
@set_func # 相当于 test2 = set_func(test2)
def test2(num):
print("-----test2----%d" % num)
test1(100) # 可通俗的看做调用了call_func(100)
test2(200)
- 装饰一个函数相当于创建一个闭包。
3.4. 装饰器几时开始装饰?!
def set_func(func):
print("---开始进行装饰")
def call_func(a):
print("---这是权限验证1----")
print("---这是权限验证2----")
func(a)
return call_func
@set_func # 相当于 test1 = set_func(test1)
def test1(num):
print("-----test1----%d" % num)
# 装饰器在调用函数之前,已经被python解释器执行了,所以要牢记 当调用函数之前 其实已经装饰好了,尽管调用就可以了
# test1(100)
以上demo并没有调用执行test1函数,但会输出:---开始进行装饰
- 可见:只要函数定义前写上 @闭包外层函数名 就开始装饰了,而不是被装饰的函数执行时才开始装饰。
- 即只要遇到装饰器就会执行,装饰器在调用函数之前,,已经被python解释器执行了。
- 所以要牢记 当调用函数之前 其实已经装饰好了,尽管调用就可以了
3.5. 对不定长参数的函数进行装饰!
def set_func(func):
print("---开始进行装饰")
def call_func(*args, **kwargs):
print("---这是权限验证1----")
print("---这是权限验证2----")
# func(args, kwargs) # 不行,相当于传递了2个参数 :1个元组,1个字典
func(*args, **kwargs) # 做实参时写上*或**会拆包(写args相当与直接传一个元祖或字典参数)
return call_func
@set_func # 相当于 test1 = set_func(test1)
def test1(num, *args, **kwargs):
print("-----test1----%d" % num) # num替换%d
print("-----test1----" , args) # 先打印逗号前的部分,再打印逗号后的args元祖
print("-----test1----" , kwargs)
test1(100)
test1(100, 200)
test1(100, 200, 300, mm=100)
其实,对于有0个及以上的函数装饰时,直接写不定长参数,就可以达到通用的效果。注意参数再传递时拆包再传。
注:*args, **kwargs参数作为实参再传给给别参数时,写上*或**才会拆包。而传args或kwargs相当与直接传一个元祖或字典参数。
3.6. 对有返回值的不定长参数函数进行装饰(重要)
该Demo的装饰器对不定长参数和有无返回值的函数都通用,是通用装饰器。
def set_func(func):
print("---开始进行装饰")
def call_func(*args, **kwargs):
print("---这是权限验证1----")
print("---这是权限验证2----")
# func(args, kwargs) # 不行,相当于传递了2个参数 :1个元组,1个字典
return func(*args, **kwargs) # 做实参时写上*或**会拆包(写args相当与直接传一个元祖或字典参数)
return call_func
@set_func # 相当于 test1 = set_func(test1)
def test1(num, *args, **kwargs):
print("-----test1----%d" % num)
print("-----test1----" , args)
print("-----test1----" , kwargs)
return "ok"
@set_func
def test2():
pass
ret = test1(100)
print(ret)
ret = test2()
print(ret)
ret = test1(100)中的test1指向闭包的函数call_func。
test1(100)调用过程:
test1 ---调用--->
call_func ---func(对应实参test1)会调用--->
test1 ---test1返回ok至--->
call_func ---返回 test1返回的ok ,并赋值给--->
变量ret
由函数test2可见,没有返回值的函数其装饰器写上 return 也没有影响,因为都会返回None。
4. 多个装饰器对同一个函数装饰
def add_privilege_1(func):
print("---开始进行装饰权限1的功能---")
def call_func(*args, **kwargs):
print("---这是权限1验证----")
return func(*args, **kwargs)
return call_func
def add_privilege_2(func):
print("---开始进行装饰权限2的功能---")
def call_func(*args, **kwargs):
print("---这是权限2验证----")
return func(*args, **kwargs)
return call_func
@add_privilege_1 # 等价于 test1 = add_privilege_1(test1)
@add_privilege_2 # 等价于 test1 = add_privilege_2(test1)
def test1():
print("------test1------")
test1()
输出:
---开始进行装饰权限2的功能---
---开始进行装饰权限1的功能---
---这是权限1验证----
---这是权限2验证----
------test1------
由输出可见,分析:
- 用多个装饰器装饰一个函数时,先装下面的,后装上面的,类似脱衣原则。
- 比如,demo中 @add_privilege_2离函数test1更近,先装饰。@add_privilege_2离函数函数test1第二进,第二个装饰。
- @add_privilege_2 等价于 test1 = add_privilege_2(test1),@add_privilege_1 等价于 test1 = add_privilege_1(test1) (注意:此时作为参数的 test1 的指向是装饰器@add_privilege_2执行后有新指向的test1,其不再指向原来的 test1 函数...),装饰完之后,test1指向最后装饰的 func。
- 故,执行 test1() 时,先执行写在上面的,后执行写在下面的,与装饰顺序相反。
多个装饰器的意义:当一个原来的函数需要添加多个功能时,可以使用多个装饰器来添加功能,并保证一个装饰器一个功能。
多个装饰器对同一个函数装饰 应用Demo
用装饰器对原函数返回的值加上<h1></h1> 标签和<td></td>标签。
def set_func_1(func):
def call_func():
# "<h1>haha</h1>"
return "<h1>" + func() + "</h1>"
return call_func
def set_func_2(func):
def call_func():
return "<td>" + func() + "</td>"
return call_func
@set_func_1
@set_func_2
def get_str():
return "haha"
print(get_str())
输出: <h1><td>haha</td></h1>
输出结果再次验证了装饰器的执行顺序(注:装饰顺序相反)。
5. 装饰器带参数,在原有装饰器的基础上,设置外部变量
实例和注释如下:
from time import ctime, sleep
def timefun_arg(pre="hello"):
def timefun(func):
def wrapped_func():
print("%s called at %s %s" % (func.__name__, ctime(), pre))
return func()
return wrapped_func
return timefun
# 下面的装饰过程
# 1. 调用timefun_arg("test")
# 2. 将步骤1得到的返回值,即time_fun返回, 然后time_fun(foo)
# 3. 将time_fun(foo)的结果返回,即wrapped_func
# 4. 让foo = wrapped_fun,即foo现在指向wrapped_func
@timefun_arg("test") # 等价于foo = timefun_arg("test")(foo)
def foo():
print("I am foo")
foo()
sleep(2)
foo()
# 执行时可以理解为:foo() == timefun_arg("test")(foo)() == time_fun(foo)() == wrapped_func()
输出:
foo called at Fri Feb 28 14:10:00 2020 test
I am foo
foo called at Fri Feb 28 14:10:02 2020 test
I am foo
6. 类装饰器(了解)
装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重写了 __call__()
方法,那么这个对象就是callable的。
# def set_func_1(func):
# def call_func():
# # "<h1>haha</h1>"
# return "<h1>" + func() + "</h1>"
# return call_func
class Test(object):
def __init__(self, func):
self.func = func
def __call__(self):
print("这里是装饰器添加的功能.....")
return self.func()
@Test # 相当于get_str = Test(get_str) ,等号右边类名()意味着创建了一个实例对象(get_str再指向实例对象),get_str传给了类的__init__方法
def get_str():
return "haha"
print(get_str()) # 实例对象() 会调用__call__方法
输出:
这里是装饰器添加的功能.....
haha
-----end-----