1. 装饰器介绍
1.1 什么是装饰器
器就是工具,可以定义成函数,比如我们生活中的捕鼠器,就是捕鼠的工具
装饰指的是为其他事物添加额外的东西点缀
那么装饰器就是装饰的工具,函数可以是工具,以后我们学的类也是工具,他们都是包含一定的功能
装饰器指的是定义一个函数,该函数是用来为其他函数添加额外的功能
1.2 为什么要用装饰器
我知道有些人肯定惯性思维认为,我给函数添加新的功能,直接在函数体里面更改不就好了
为什么还非要用装饰器那么麻烦的写法,但是你要知道做开发,一般测试过已将没问题的源代码
是不允许修改的,你想拓展新功能可以,但是不要在源代码里面改他,特别容易出bug
开放封闭原则
-
开放 : 指的是对拓展功能是开放的
-
封闭 : 指的是对修改源代码是封闭的
装饰器就是在不修改被装饰器对象源代码以及调用方式的前提下为被装饰对象添加新功能
2. 装饰器的诞生
接下来我们提一个简单的需求,然后用原来学过的知识,尝试看看能不能实现,进而推出装饰器的语法。
2.0 需求
定义一个index函数,但是要在不改变函数体代码和调用方式的前提下为其拓展一个统计函数运行时间的功能
import time # 导入time函数
time.sleep(3) # 暂停3s
time.time() # 返回一个时间戳 单位秒 从1970-01-01 到现在的秒数
import time
def index(x,y):
time.sleep(3)
print('index=> %s %s' % (x, y))
2.1 方案一
def index(x, y):
start = time.time()
print('index=> %s %s' % (x, y))
stop = time.time()
print('运行了 %s 秒' % (stop-start))
index(100, 200)
失败 : 虽然没有改变调用方式也拓展了新功能 , 但是修改了源代码
2.2 方案二
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
start = time.time()
index(100, 200)
stop = time.time()
print('运行了 %s 秒' % (stop - start))
失败 : 虽然没有改变原函数的调用方式 , 也没有修改函数体代码 , 但是带来了一个新的问题就是 , 代码冗余 , 每次
调用函数都要加上这三句代码
start = time.time()
index(100, 200)
stop = time.time()
print('运行了 %s 秒' % (stop - start))
2.3 方案三
既然代码冗余,那么我们可以用函数来解决代码冗余的问题
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
def wrapper():
start = time.time()
index(100, 200)
stop = time.time()
print('运行了 %s 秒' % (stop - start))
wrapper()
失败 :这次代码虽然不冗余了,也没有修改源代码,但是代码的调用方式改变了,而且参数也被写死了
2.4 方案三的优化1
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
def wrapper(*args,**kwargs):
start = time.time()
index(*args,**kwargs)
stop = time.time()
print('运行了 %s 秒' % (stop - start))
wrapper(22,33) # 只要按照index的形参格式传参就行了,想传几就传几
失败 : 优化了index需要接收的参数
,没有写死,但是这个写死了只为index函数
拓展功能,如果换成变的函
数,还要在wrapper
里修改函数名
2.5 方案三的优化2
这个时候我们在调用的时候把函数名传进来不就可以了吗 ?
wrapper(22,33,'函数名')
看上去不错,但是你不要忘记,为wrapper传参,要按照index的形参格式传递,否则在wrapper函数体内调用
index函数报错
这个时候就要考虑另外一种传参的方式了---->闭包
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
def outer():
func = index
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
stop = time.time()
print('运行了 %s 秒' % (stop - start))
return wrapper # 一定要返回wrapper,否则你在全局就无法调用wrapper了,更别提调用index了
f = outer() # f指向的内存地址就是outer函数返回值指向的内存地址即wrapper
f(11,22) # 调用f就是调用wrapper
上面存在的问题都解决了,但是调用函数改变了,但是你写f可以,写成index也是可以的
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
def outer():
func = index
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
stop = time.time()
print('运行了 %s 秒' % (stop - start))
return wrapper # 一定要返回wrapper,否则你在全局就无法调用wrapper了,更别提调用index了
index = outer() # index指向的内存地址就是outer函数返回值指向的内存地址即wrapper
index(11,22) # 调用index就是调用wrapper
现在 : 调用方式没有改变,函数体的代码也没有修改,这个时候只需要把函数名设置成参数,传递进来,就可以
为其他函数装饰了
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
def outer(func): # 传递进来的函数地址赋值给func
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
stop = time.time()
print('运行了 %s 秒' % (stop - start))
return wrapper # 一定要返回wrapper,否则你在全局就无法调用wrapper了,更别提调用index了
index = outer(index) # 非参数index指向的内存地址就是outer函数返回值指向的内存地址即wrapper
index(11, 22) # 调用index就是调用wrapper
2.6 方案三的优化3
离完成就差一步了,函数的返回值,如果我想取到index函数的返回值怎么取呢
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
return 1
res = index(11,22) # 取函数的返回值
print(res)
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
return 1
def outer(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs) #取到函数返回值
stop = time.time()
print('运行了 %s 秒' % (stop - start))
return res
return wrapper # 一定要返回wrapper,否则你在全局就无法调用wrapper了,更别提调用index了
index = outer(index) # 非参数index指向的内存地址就是outer函数返回值指向的内存地址即wrapper
ret = index(11, 22) # 调用index就是调用wrapper
print(ret) # ret是wrapper的返回值,wrapper的返回值是原index的返回值
# 所以ret就是原index的返回值
3. 语法糖
糖
大家都知道吧,也都吃过吧,别tmd说你从小到大没吃过糖,糖吃了很甜,会开心
所以语法糖就是你用了会很方便,很开心
不使用语法糖 :
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
return 1
def home():
time.sleep(2)
return 2
def outer(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs) #取到函数返回值
stop = time.time()
print('运行了 %s 秒' % (stop - start))
return res
return wrapper # 一定要返回wrapper,否则你在全局就无法调用wrapper了,更别提调用index了
index = outer(index) # 非参数index指向的内存地址就是outer函数返回值指向的内存地址即wrapper
ret = index(11, 22) # 调用index就是调用wrapper
print(ret) # ret是wrapper的返回值,wrapper的返回值是原index的返回值
# 所以ret就是原index的返回值
# 如果你想装饰home函数就要在调用home函数前加上一句home = outer(home)
home = outer(home)
ret1 = home()
print(ret1)
问题 : 这个时候就会出现一个问题,那就是你如果想要装饰那个函数,就需要在调用前面加上一句偷梁换柱的语法
``函数名 = outer(函数名)`,但是python推崇的就是语法优雅,怎么优雅怎么来,于是龟叔在装饰器中创造了语
法糖,这个东东。你只需要在函数定义的时候加上语法糖,调用的时候就不需要写上那一句偷梁换柱的语法了
但是装饰器要定义在被装饰函数之前
语法
-
@装饰器函数名
被装饰函数
# 当python扫描到@,就会把@下面的函数名,当做参数传递给@后面的函数,并赋值给下面的函数名
@print ==> home = print(home)
def home():
pass
def outer(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs) #取到函数返回值
stop = time.time()
print('运行了 %s 秒' % (stop - start))
return res
return wrapper # 一定要返回wrapper,否则你在全局就无法调用wrapper了,更别提调用index了
@outer # ==> index = outer(index),但是原index函数并没有调用
def index(x, y):
time.sleep(3)
print('index=> %s %s' % (x, y))
return 1
@outer # ==> home = outer(home),但是原home函数并没有调用
def home():
time.sleep(2)
return 2
# 直接调用
ret = index(11, 22)
print(ret)
# 直接调用
ret1 = home()
print(ret1)
4. 无参装饰器模板
def outer(func)
def wrapper(*args,**kwargs):
# 函数执行前拓展的功能
res = func(*args,**kwargs)
# 函数执行完拓展的功能
return res
return wrapper
# wrapper函数的功能就是调用调用原函数,拓展新功能
5. 多个装饰器装饰一个函数
你有可能在面试题中遇到多个装饰器,装饰多个函数,但是你也不要慌,慢慢一步一步分析就可以了
@deco3
@deco2
@deco1
def index():
pass
叠加多个装饰器也无特殊之处,上述代码语义如下:
index=deco3(deco2(deco1(index))) # 先执行最里面的deco1(index)
6. 有参装饰器
同样我们仍然通过一个需求,来引出有参装饰器,同样其中也解释了为什么要有有参装饰器,有参装饰器有什么用
6.1 基本需求
通过装饰器,为index,home,transfer三个函数加入登录验证功能
def auth(func):
def wrapper(*args, **kwargs):
name = input('your name>>>: ').strip()
pwd = input('your password>>>: ').strip()
if name == "ymn" and pwd == '123':
print("登录成功")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
return wrapper
@auth
def index(x, y):
print('index->>%s : %s ' % (x, y))
@auth
def home(x, y):
print('home->>%s : %s ' % (x, y))
@auth
def transfer(x, y):
print('transfer->>%s : %s ' % (x, y))
ok,现在已经通过无参装饰器实现了用户登录验证的拓展功能了,但是我要提升需求了,哈哈哈哈
6.2 升级需求
假如这个登录验证的方式有很多种,比如是从文件里面验证用户和密码是否正确,从数据库中,或者是从ldap中
那么我要求你,对你的代码进行升级,满足我的需求,无论是哪种验证你都能做,那你肯定说这还不简单,加一个
判断不就好了,废话不多说上代码
def auth(func):
def wrapper(*args, **kwargs):
name = input('your name>>>: ').strip()
pwd = input('your password>>>: ').strip()
# 通过db_type变量判断是哪种验证方式
if db_type == "file":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于文件的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
elif db_type == "mysql":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于mysql数据库的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
elif db_type == "ldap":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于ldap的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
else:
print('不支持该db_type')
return wrapper
@auth
def index(x, y):
print('index->>%s : %s ' % (x, y))
index(11,22)
但是上面就出现一个问题,db_type
这个参数通过什么传进来?那还不容易直接传,那对不起了wrapper
函数
不能随便设置形参,因为wrapper的参数
要看index参数定义
时规定了哪些参数,好那我通过auth函数传进来
我在auth函数上多写一个参数,不就能接受外界传进来的db_type了吗?哈哈哈哈哈,又不好意思,@语法糖的
限制,限制装饰器函数,只能传一个被装饰的函数,你可以给auth函数设置形参,但是@语法糖又不能传其他
参数,所以肯定报错,行不通。好,那我不用语法糖,我偷梁换柱。
def auth(func, db_type):
def wrapper(*args, **kwargs):
name = input('your name>>>: ').strip()
pwd = input('your password>>>: ').strip()
if db_type == "file":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于文件的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
elif db_type == "mysql":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于mysql数据库的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
elif db_type == "ldap":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于ldap的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
else:
print('不支持该db_type')
return wrapper
def index(x, y):
print('index->>%s : %s ' % (x, y))
def home(x, y):
print('home->>%s : %s ' % (x, y))
index = auth(index, "file") # 偷梁换柱的写法
index(1, 2)
home = auth(home, "mysql")
home(1, 2)
但是这样就违背了python优雅的宗旨 , 一两个函数还好,但是如果有几十万个呢?这样每个函数都要在调用前写
上一个偷梁换柱的代码,很冗余。tmd,不就是传参吗?难道你又忘记还有一种函数传参方式----闭包
废话不多说,上代码
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":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于文件的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
elif db_type == "mysql":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于mysql数据库的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
elif db_type == "ldap":
# 从文件中取账号密码进行验证
if name == "ymn" and pwd == '123':
print("基于ldap的验证")
res = func(*args, **kwargs)
return res
else:
print('user or password error')
else:
print('不支持该db_type')
return wrapper
return deco
@auth("file")
def index(x, y):
print('index->>%s : %s ' % (x, y))
@auth("mysql")
def home(x, y):
print('home->>%s : %s ' % (x, y))
@auth("ldap") # ===> @deco ==> transfer=deco(transfer)
def transfer(x, y):
print('transfer->>%s : %s ' % (x, y))
index(1, 2)
home(10, 20)
transfer(11, 22)
有参装饰器 , 一定要先看带括号的 , 即@右边的函数先执行 , 然后继续装饰下面的函数
实际上三层就已经可以满足基本所有需求了 , 因为最外面一层是不受@语法糖的限制 , 随便你传任意多种参数
6.3 有参装饰器模板
def outer(*args, **kwargs):
def deco(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
return deco
7. 还原被装饰函数属性
函数默认是有很多属性的 , 比如说注释信息,以及他的名字,有人说定义的时候不就设置好了函数名吗?那你可以
看看被装饰后函数的这些属性
# 没有被装饰前原的函数属性
def index(x, y):
"这是主页"
print("index--> %s:%s" % (x, y))
print(index.__name__) # index
print(index.__doc__) # 这是主页
# 被装饰之后原函数的属性
def outter(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
@outter
def index(x, y):
"这是主页"
print("index--> %s:%s" % (x, y))
print(index.__name__) # wrapper
print(index.__doc__) # None
因为index
一旦被装饰,那么他实际上指向的内存地址就是wrapper的内存地址
了,自然属性也变成wrapper函数的属性
了。所以你想伪装的像原函数,那么就要在调用wrapper函数前
把index函数的属性赋值给它
上代码 :
# 被装饰之后原函数的属性
def outter(func):
def wrapper(*args, **kwargs):
wrapper.__name__ = index.__name__
wrapper.__doc__ = index.__doc__
res = func(*args, **kwargs)
return res
return wrapper
@outter
def index(x, y):
"这是主页"
print("index--> %s:%s" % (x, y))
print(index.__name__) # index
print(index.__doc__) # 这是主页
哇,你也太优秀了,这种操作都能想到,但是你认为一个函数就只有__name __
he __doc__
属性了吗?实际上有
很多,如果全部自己赋值这是不现实的问题,所以python给你了解决方案
from functools import wraps # 导入wraps
def outter(func):
# @wraps(func) # wraps装饰器会把func的属性,即即将被传进来的index函数的属性收集起来,然 后赋值给wrapper
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
@outter
def index(x, y):
"这是主页"
print("index--> %s:%s" % (x, y))
print(index.__name__) # index
print(index.__doc__) # 这是主页
8. 多个装饰器装饰一个函数
def deco(num):
def outer1(func):
def wrapper1(*args, **kwargs):
print("wrapper1====>")
ret = func(*args, **kwargs)
return ret
return wrapper1
return outer1
def outer2(func):
def wrapper2(*args, **kwargs):
print("wrapper2====>")
ret = func(*args, **kwargs)
return ret
return wrapper2
def outer3(func):
def wrapper3(*args, **kwargs):
print("wrapper3====> ")
ret = func(*args, **kwargs)
return ret
return wrapper3
@outer3
@outer2
@deco(88888)
def index(x, y):
print('index====> %s %s' % (x, y))
index(11,22)
首先多个装饰器加载顺序
是自下而上
的,带括号的先执行
@outer3
@outer2
@deco(88888)
def index(x, y):
print('index====> %s %s' % (x, y))
# 1.先执行deco(88888),这个是函数名+括号,调用函数,返回返回值outer1
# 2.此时便是
@outer3
@outer2
@outer1
def index(x,y):
print('index====> %s %s' % (x, y))
# outer1装饰index,即执行函数index = outer1(index),outer1执行返回wrapper1
# index此时指向wrapper1的内存地址
# 3.
@outer3
@outer2
def index():
pass
# outer2装饰index,即执行函数,index = outer2(index指向的内存地址->wrapper1),outer2执行返回wrapper2
# 此时index指向wrapper2的内存地址
# 4.
@outer3
def index():
pass
# outer3装饰index,即执行函数,index = outer3(index指向的内存地址->wrapper2),outer3执行返回wrapper3
# 此时index指向wrapper3的内存地址
# 所以你调用index会先调用wrapper3函数, 打印 "wrapper3====> "
# 然后wrapper3中的fun是wrapper2,所以会执行wrapper2函数,打印"wrapper2====>"
# 然后wrapper2中的fun是wrapper1,所以会执行wrapper1函数,打印"wrapper1====>"
# 然后wrapper1中的func是index,所以再执行index函数,打印"index====> 11 22"