Python-装饰器
一、必备前置知识点
-
*args, **kwargs
def index(x, y): print(x, y) def wrapper(*args, **kwargs): index(*args, **kwargs) wrapper(y=222, x=111) # index(y=222,x=111) # 运行结果: # 111 222
给wrapper函数传递的参数实际上是给index函数传,遵循的是index函数的参数传递规则。
-
名称空间与作用域
名称空间的的"嵌套"关系是在函数定义阶段,即检测语法的时候确定的。
-
函数对象
可以把函数(函数的内存地址)当做参数传入
可以把函数(函数的内存地址)当做返回值返回
def index(): print("123") return 123 def foo(func): return func foo(index)() # 123
-
函数的嵌套定义
def outter(func): # 可以传入一个函数 def wrapper(): pass return wrapper # 可以返回一个函数
-
闭包函数
def outter(): x = 111 def wrapper(): print(x) return wrapper f = outter() f() # outter()() # 111
两种传参的方式
传参的方式一:通过参数的形式为函数体传值
def wrapper(x): print(x) wrapper(1) wrapper(2) wrapper(3)
传参的方式二:通过闭包的方式为函数体传值
def outter(x): def wrapper(): print(x) return wrapper # return outter内的wrapper那个函数的内存地址 wrapper = outter(1) # 这里的变量名wrapper是全局名称空间里的,跟outter函数里存在于局部名称空间里的函数名wrapper不是一个东西。但都指向同一个内存地址。 wrapper() # 1 outter(2)() # 2
二、装饰器的基本概念
-
什么是装饰器
器指的是工具,可以定义成函数;
装饰指的是为其他事物添加额外的东西点缀。
合到一起的解释:装饰器指的就是一个函数,该函数是用来为其他函数添加额外功能的。
-
为何要用装饰器
对已完成布置上线的软件,不可能为更新功能就关闭下线。
开放封闭原则:
- 开放:指对拓展功能是开放的
- 封闭:指对修改源代码是封闭的
如何在不修改源代码的前提下增加拓展新的功能?
装饰器就是在不修改被装饰对象源代码以及调用方式的前提下,为被装饰对象添加新功能的方法。
三、装饰器的实现
无参装饰器
def index(x,y):
print("index %s %s" % (x,y))
index(111,222)
# index(y=222,x=111)
# index(111,y=222)
需求:在不修改index函数的源代码以及调用方式的前提下为其添加统计运行时间的功能。
解决方案一:
import time
def index(x, y):
start = time.time()
print('index %s %s' % (x, y))
stop = time.time()
print(stop - start)
index(111, 222)
# 运行结果:
# index 111 222
# 0.0010006427764892578
失败。
问题:没有修改被装饰对象的调用方式,但是修改了其源代码。
解决方案二:
import time
def index(x, y):
print('index %s %s' % (x, y))
start = time.time()
index(111, 222)
stop = time.time()
print(stop - start)
失败。
问题:没有修改被装饰对象的调用方式,也没有修改其源代码,并且加上了新功能,但当调用次数多了之后,就得在每一次调用的位置重复添加相关代码,会造成代码冗余。
解决方案三:
import time
def index(x, y):
print('index %s %s' % (x, y))
def wrapper():
start = time.time()
index(111, 222)
stop = time.time()
print(stop - start)
wrapper()
失败。
问题:虽然解决了方案二的代码冗余问题,但带来了一个新问题,即函数的调用方式改变了。
针对于方案三,我们进行进一步的优化。
优化的大方向:如何在方案三的基础上不改变函数的调用方式
方案三的优化一:
import time
def index(x, y):
print('index %s %s' % (x, y))
def wrapper(a, b):
start = time.time()
index(a, b)
stop = time.time()
print(stop - start)
wrapper(1, 2)
问题:虽然解决了调用方式不发生改变的问题,但如果我们需要给index函数再加上一个形参呢?就还得对wrapper函数进行源代码的改写。
我们说,wrapper函数装饰功能写好了之后,我们就最好不再改它了。能否将这个装饰函数写的更完全一些呢?让它具备更好的支持作用。
方案三的优化二:将index函数的参数写活了
import time
def index(x, y, z):
print('index %s %s %s' % (x, y, z))
def wrapper(*args, **kwargs):
start = time.time()
index(*args, **kwargs)
stop = time.time()
print(stop - start)
wrapper(3333,4444,5555)
wrapper(3333,z=5555,y=4444)
这样子的话,wrapper函数就可以接收更多的参数了。不管我们给index函数增加多少个形参,都不需要再改写wrapper函数的源代码了,只需要遵守index函数的参数传递规则就可以成功运行了。
问题:装饰器wrapper函数我们已经写好了,但现在只能给index函数使用。我们说,最好的情况是一个装饰器可以支持所有需要这个装饰器功能的函数,那我们该怎么办呢?
方案三的优化三:在优化二的基础上把被装饰对象写活了,原来只能装饰index函数
import time
def index(x, y, z):
print('index %s %s %s' % (x, y, z))
def outter(func):
# func = index的内存地址
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs) # 相当于index的内存地址加括号
stop = time.time()
print(stop - start)
return wrapper
# print(index) # <function index at 0x000002BF6DDE6160>
index = outter(index) # index=wrapper的内存地址
# 要注意:经过这一步之后,index就已经不是原来的index了,是在原有功能基础上加上装饰器功能的新产物。
# 而且相比以前的index,现在的其id也发生了改变。如果没有这一步,装饰器就起不到作用。
# print(index) # <function outter.<locals>.wrapper at 0x000002BF7EAD4700>
index(1, y=2, z=3)
我们利用闭包函数的原理来对装饰器进行了进一步优化,这下装饰器就可以支持所有函数使用了,只需要传进来被修饰函数的内存地址就可以。
举例如下:
import time
def index(x, y, z):
print('index %s %s %s' % (x, y, z))
def hello(name):
print('Nice to meet you, %s' % name)
def outter(func):
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs) # 相当于传进来的函数的内存地址加括号
stop = time.time()
print(stop - start)
return wrapper
index = outter(index) # index=wrapper的内存地址
index(1, y=2, z=3)
hello = outter(hello) # hello=wrapper的内存地址
hello("Python")
# 运行结果:
# index 1 2 3
# 0.0010006427764892578
# Nice to meet you, Python
# 0.0010006427764892578
# 要注意:虽然index和hello在理解上都指向wrapper的内存地址,但实质上两者并不相同。
# 两个不同的原函数经过装饰器装饰之后,相当于从一个工厂加工完成,打上了相同的工厂标签,但实质上还是两个不同的产品。
print(index) # <function outter.<locals>.wrapper at 0x000001DCF4DC5670>
print(hello) # <function outter.<locals>.wrapper at 0x000001DCF4DC55E0>
问题:像下面的这种情况,原函数有返回值,在经过装饰之后,返回值却没有了(变成None)。
def index():
print('index函数执行')
return 'over'
def outter(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
print('装饰器起作用了')
return wrapper
res0 = index() # index函数执行
print(res0) # over
index = outter(index)
res1 = index() # index函数执行 \n 装饰器起作用了
print(res1) # None
我们想让被装饰过的函数保留它原来的返回值,使其不要变成None,该怎么办?
方案三的优化四:将wrapper做的跟被装饰对象一模一样,以假乱真
def index():
print('index函数执行')
return 'over'
def outter(func):
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs) # 接住原函数的返回值
print('装饰器起作用了')
return ret # 返回原函数的返回值
return wrapper
res0 = index() # index函数执行
print(res0) # over
index = outter(index)
res1 = index() # index函数执行 \n 装饰器起作用了
print(res1) # over
到这里,我们终于成功了,而且是万无一失的成功。
import time
def index(x, y, z):
print('index %s %s %s' % (x, y, z), end='|||')
return x
def hello(name):
print('Nice to meet you, %s' % name, end='|||')
def outter(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
stop = time.time()
print(stop - start)
return res
return wrapper
hello = outter(hello)
index = outter(index)
res = hello('PyCharm') # Nice to meet you, PyCharm|||0.0
print('返回值--》', res) # 返回值--》 None
ret = index(1, 2, 3) # index 1 2 3|||0.0
print('返回值--》', ret) # 返回值--》 1
经历了四次优化,可谓是被折磨的苦不堪言,屁事儿怎么这么多啊。这个时候,一颗糖果从天而降…上面写着“语法糖”三个字。
语法糖:披荆斩棘后的赏赐
每次装饰对象的时候,都要先把被装饰的那个对象送给outter加工一下,要不然装饰器就起不了作用。然后还要覆盖命名,便于我们使用。
就像这样:
hello = outter(hello)
index = outter(index)
语法糖的存在帮我们简化了这一步骤,直接在被装饰函数的头顶上写上“@装饰器名字”,再使用这个函数的时候就直接是被装饰过后的状态(使用语法糖需要注意装饰器要写在被装饰函数之前,要不然会报错的)。
def outter(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print("装饰完成!")
return res
return wrapper
@outter # index = outter(index)
def index(x, y, z):
print('index %s %s %s' % (x, y, z), end='、')
return x
@outter # hello = outter(hello)
def hello(name):
print('Nice to meet you, %s' % name, end='、')
hello('PyCharm') # Nice to meet you, PyCharm、装饰完成!
index(1, 2, 3) # index 1 2 3、装饰完成!
多个语法糖叠加的时候:
# 多个装饰器叠加,加载顺序是从内向外,先加载离头顶最近的那个。
@deco1 # index=deco1(deco2.wrapper的内存地址)
@deco2 # index=deco2(deco3.wrapper的内存地址)【新index指向deco2内wrapper的内存地址】
@deco3 # index=deco3(index)【index指向deco3内wrapper的内存地址】
def index():
pass
总结无参装饰器的模板
def outter(func):
def wrapper(*args, **kwargs):
# 1、调用原函数
# 2、为其增加新功能
res = func(*args, **kwargs)
return res
return wrapper
小例子:
def auth(func):
def wrapper(*args, **kwargs):
# 1、调用原函数
# 2、为其增加新功能
name = input('请输入用户名:').strip()
pwd = input('请输入密码:').strip()
if name == 'jl' and pwd == '123':
res = func(*args, **kwargs)
return res
else:
print('账号密码错误')
return wrapper
@auth
def hello():
print("欢迎光临!")
hello()
有参装饰器
有参装饰器的必备前置知识
# 由于语法糖@的限制,outter函数只能有一个参数,并且该参数是只用来接收被装饰对象的内存地址。
# wrapper函数的参数是用来给原函数传递参数使用,也就是说给wrapper传递的参数会原封不动的传递给原函数。
def outter(func):
# func = 函数的内存地址
def wrapper(*args,**kwargs):
res=func(*args,**kwargs)
return res
return wrapper
@outter # index=outter(index) # index=>wrapper
def index(x,y):
print(x,y)
所以我们得到一个结论:outter的参数不能动,wrapper的参数也不能动。
有参装饰器的实现
如果我们想要制作一个带有认证功能的装饰器,现需要给装饰器多传一个参数进来。但我们知道已有的参数是不能动的,该怎么办?
山炮回答:既然语法糖不允许多传一个参数进来,那就不用语法糖,多加个参数就没限制了。
def auth(func, func_name):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
if func_name == 'index':
print('已装饰index函数')
elif func_name == 'home':
print('已装饰home函数')
elif func_name == 'hello':
print('已装饰hello函数')
else:
print('已装饰其他函数')
return wrapper
def index(x, y):
print('index->>%s:%s' % (x, y), end='~~~')
def home(name):
print('home->>%s' % name, end='~~~')
def hello():
print('hello', end='~~~')
index = auth(index, 'index')
home = auth(home, 'home')
hello = auth(hello, 'hello')
index(1, 2) # index->>1:2~~~已装饰index函数
home('翻斗花园') # home->>翻斗花园~~~已装饰home函数
hello() # hello~~~已装饰hello函数
结果证明,山炮的策略是可行的。不使用语法糖,后退一步确实可以多传一个参数进来帮助我们完成认证功能。
那么规定必须使用语法糖呢,又该如何做?
山炮再加一层闭包函数试了试。
def auth(func_name):
def deco(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
if func_name == 'index':
print('已装饰index函数')
elif func_name == 'home':
print('已装饰home函数')
elif func_name == 'hello':
print('已装饰hello函数')
else:
print('已装饰其他函数')
return wrapper
return deco
def index(x, y):
print('index->>%s:%s' % (x, y), end='~~~')
def home(name):
print('home->>%s' % name, end='~~~')
def hello():
print('hello', end='~~~')
reco = auth('index') # reco --> deco
index = reco(index) # index --> wrapper ——》 可写成语法糖
index(1, 2) # index->>1:2~~~已装饰index函数
reco = auth('home')
home = reco(home)
home('翻斗花园') # home->>翻斗花园~~~已装饰home函数
reco = auth('hello')
hello = reco(hello)
hello() # hello~~~已装饰hello函数
def auth(func_name):
def deco(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
if func_name == 'index':
print('已装饰index函数')
elif func_name == 'home':
print('已装饰home函数')
elif func_name == 'hello':
print('已装饰hello函数')
else:
print('已装饰其他函数')
return wrapper
return deco
reco = auth('index')
@reco
def index(x, y):
print('index->>%s:%s' % (x, y), end='~~~')
reco = auth('home')
@reco
def home(name):
print('home->>%s' % name, end='~~~')
reco = auth('hello')
@reco
def hello():
print('hello', end='~~~')
index(1, 2) # index->>1:2~~~已装饰index函数
home('翻斗花园') # home->>翻斗花园~~~已装饰home函数
hello() # hello~~~已装饰hello函数
问题:再加了一层闭包函数,果然可以继续使用语法糖了,可是带来了一个新的问题,那就是每次都要手动进行最外部函数的参数传递,才能使用里面的语法糖。
解决方法:替换语法糖内容。
def auth(func_name):
def deco(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
if func_name == 'index':
print('已装饰index函数')
elif func_name == 'home':
print('已装饰home函数')
elif func_name == 'hello':
print('已装饰hello函数')
else:
print('已装饰其他函数')
return wrapper
return deco
# 因为auth('index')的结果返回的是deco函数的内存地址,所以@auth('index')就相当于@deco
@auth('index') # @deco 相当于==》 index = deco(index) # 故而index --> wrapper(指向wrapper的地址)
def index(x, y):
print('index->>%s:%s' % (x, y), end='~~~')
@auth('home')
def home(name):
print('home->>%s' % name, end='~~~')
@auth('hello')
def hello():
print('hello', end='~~~')
index(1, 2) # index->>1:2~~~已装饰index函数
home('翻斗花园') # home->>翻斗花园~~~已装饰home函数
hello() # hello~~~已装饰hello函数
所以我们可以总结出有参装饰器的模板:
def 有参装饰器(x,y,z):
def outter(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
return outter
@有参装饰器(1,y=2,z=3)
def 被装饰对象(a, b):
pass
被装饰对象(1, 2)
四、补充
函数具有各种各样的属性,经过装饰器加工过后的产物毕竟还是与原函数从根本上不是一个东西的。我们虽然通过语法糖(覆盖赋值)偷梁换柱的方法让我们以原函数名就能够使用加工过后的函数,但函数的属性却还是和原函数不一样的。
偷梁换柱,即将原函数名指向的内存地址偷梁换柱成wrapper函数,所以我们应该将wrapper做的跟原函数一样才行。
偷梁换柱之后:
- index的参数什么样子,wrapper的参数就应该什么样子 ==》 *args, **kwargs
- index的返回值什么样子,wrapper的返回值就应该什么样子 ==》 res = func(*args, **kwargs) 加 return res
- index的属性什么样子,wrapper的属性就应该什么样子 ==》 from functools import wraps
from functools import wraps
def outter(func):
@wraps(func)
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
@outter
def index(x, y):
print(x, y)
通过from functools import wraps,在wrapper函数头上加@wraps(func),就可以让wrapper函数的各种属性和传进来的函数一模一样。