在python
的学习过程中,如果我们想进阶的话,装饰器这个概念无论如何都是绕不开的,初学者甚至还嫌弃上了,这玩意有什么用呢?看着概念又费解,还用的不畅快,不如不学,正所谓技多不压身,学了顶多掉头发不是嘛,没其他坏处O(∩_∩)O而且,这个是从初学者向高阶晋级的一道坎
在python
中的装饰器是为对象提供一些额外的功能,这些对象可以是类、方法、也可以是函数等等。装饰器本身就是一个闭包的一种应用,而闭包就是在函数中再嵌套一个函数,并且可以引用外包函数的变量。装饰器就是用于扩展原来函数的一种函数,类似原函数的插件,按照现在的说法应该是原函数plus,这个插件函数的返回值比较特殊,也是一个函数,其他的也就在使用上,需要在相对应的函数头上加@demo
就行了
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
什么叫装饰,就是装点、提供一些额外的点缀,前面说的装饰器的相关概念呢,确实有点绕口啥的,我这里举个简单的例子,来辅助一下理解,比如说女朋友跟你出来约会,本来人家就天生丽质了,还要精心打扮一下,头上戴个小发夹,手上小手提,还要有个小手环,还要再问一下:“宝儿,今天我有什么不一样?”
兄弟们这个回答有啥好犹豫的:“没啥不一样啊,是更美了好像?”
这就是装饰器的本质,不改变该对象的基本特性,女票原本什么样还是怎么样,不以外在形象为转移,关键是变美了,这是新特性就是装饰器带来的,比如说女票的小发夹带着更俏皮了,小手环袋子就温婉了一点,不是嘛?
在使用装饰器的过程中,我们可以更进一步的体会到面向对象编程的开放封闭原则的应用
开放封闭原则主要体现在两个方面:
假设你写了几个函数,有一天老板心血来潮说,你把每个函数的运行时长(结束时间-开始时间)统计下,作为一个python101练习生的你可能会这样写
原始函数
def func_a():
print("hello")
def func_b():
print("naug")
if __name__ == '__main__':
func_a()
func_b()
计算运行时间
作为python101练习生的你,大概率会这样写
import time
def func_a():
start = time.time()
print("love")
time.sleep(0.8)
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
def func_b():
start = time.time()
print("naug")
time.sleep(0.8)
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
if __name__ == '__main__':
func_a()
func_b()
上面的代码虽然满足了领导的要求,但是如果你写的函数很多的话,每个函数都这样去添加,会显得代码很臃肿,有很多重复代码。老板看了代码大概率只会摇头和沉默,唉,毕竟是练习生还能要求啥呢,再带带吧
然后边上的一个扫地僧看了下你的代码,给你指了条明路:装饰器
装饰器可以写成函数式装饰器,也可以写成一个类装饰器,先从简单的函数装饰器开始学习。
再强调一下,python装饰器本质上就是一个函数,是闭包在python中的一种实现,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象。
runtime函数就是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。在函数上方使用@语法糖
就可以调用这个装饰器了
import time
def runtime(func):
def wrapper():
start = time.time()
f = func() # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f
return wrapper
@runtime
def func_a():
print("love")
time.sleep(0.5)
@runtime
def func_b():
print("naug")
time.sleep(0.8)
if __name__ == '__main__':
func_a()
func_b()
感受到了吗,当我们成功封装了这个装饰器,也就意味着我们构建完成一个函数运行通用计时器runtime
,通过这个计时器,我们在需要测算运行性能的函数都可以通过@runtime
的形式来进行挂载使用,从而完成对相对应的函数的运行时间测算,一次开发,多次使用,降低重复造轮子的成本,总之一个字!绝
事实上这个runtime
并不强壮,如果函数里面带有参数,那就不管用了,并且函数的参数是不固定的,这时候就需要用到*args
,**kwargs
这个组合了
import time
def runtime(func):
def wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f
return wrapper
@runtime
def func_a(girl):
print("love "+girl)
time.sleep(0.5)
@runtime
def func_b(b, c="xx"):
print("hi "+b+c)
time.sleep(0.8)
if __name__ == '__main__':
func_a("naug")
func_b("world ", c="new start")
说到__call__
,那就需要再补充一个个概念,可调用对象(callable),我们平时自定义的函数、内置函数和类都属于可调用对象,凡是可以把一对括号()应用到某个对象身上都可称之为可调用对象,判断对象是否为可调用对象可以用函数callable
。如果在类中实现了__call__
方法,那么实例对象也将成为一个可调用对象
还是原来那个计时器的例子,我们将这个方法封装成一个对象,再来体会一下类装饰器的特点
import time
class runtime(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
start = time.time()
f = self.func(*args, **kwargs) # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f
@runtime
def func_a(girl):
print("love "+girl)
time.sleep(0.5)
@runtime
def func_b(b, c="xx"):
print("hi "+b+c)
time.sleep(0.8)
if __name__ == '__main__':
func_a("naug")
func_b("world ", c="new start")
冬天到了,老板说运行的速度先不要太快,让客户先加钱,然后再以正常的速度显示,那么现在的需求是让每个函数的运行时间加50%,该如何实现呢?
这就到了装饰器的高级语法,装饰器也需要带上参数了
import time
def runtime(slowly=1):
def wrapper(func):
def inner_wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
end = time.time()
t = end-start
time.sleep((slowly-1)*t) # 延迟效果
new_end = time.time()
print("运行时长:%.4f 秒" % (new_end-start))
return f
return inner_wrapper
return wrapper
@runtime(1.5)
def func_a(girl):
print("love "+girl)
time.sleep(0.5)
@runtime(1.5)
def func_b(b, c="xx"):
print("hi "+b+c)
time.sleep(0.8)
if __name__ == '__main__':
func_a("naug")
func_b("world ", c="new start")
import time
class runtime(object):
def __init__(self, slowly=1):
self.slowly = slowly
def __call__(self, func):
def wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
end = time.time()
t = end-start
time.sleep((self.slowly-1)*t) # 延迟效果
new_end = time.time()
print("运行时长:%.4f 秒" % (new_end-start))
return f
return wrapper
@runtime(1.5)
def func_a(girl):
print("love "+girl)
time.sleep(0.5)
@runtime(1.5)
def func_b(b, c="xx"):
print("hi "+b+c)
time.sleep(0.8)
if __name__ == '__main__':
func_a("naug")
func_b("world ", c="new start")
上面的例子主要是一个很简单的应用场景,如果你用过locust
,设置权重会用到@task(1)
,如果你用过pytest
框架,使用fixture
功能的时候经常会用到@pytest.fixture(scope="function")
,也被大量使用于Flask
和Django
等web框架中,检查是否被授权去使用一个web应用的端点(endpoint)。如@login_required
也可以自己写个装饰器添加日志
以前面的runtime
为例,我们来对装饰器的运行机制来进行解析,
def runtime(func):
def wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f
return wrapper
def runtime(func):
的时候,发现这是个一等公民函数,于是把函数体加载到内存里,之后进行下一个函数的解析@runtime
的时候,程序被@
这个语法糖吸引住了,知道这是个装饰器,按规矩要立即执行的,于是程序开始运行@
后面那个名字runtime
所定义的函数。runtime
函数,开始执行装饰器的语法规则。规则是:被装饰的函数的名字会被当作参数传递给装饰函数。装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数。原来的func_a
函数被当做参数传递给了slowly
,而func_a
这个函数名之后会指向wrapper
函数。注意:
@runtime
和@runtime()
有区别,没有括号时,runtime
函数依然会被执行,这和传统的用括号才能调用函数不同,需要注意一下func_a
这个函数名(而不是func_a()
这样被调用后)当做参数传递给装饰函数runtime
,也就是:func = func_a
,@runtime
等于runtime(func_a)
,实际上传递了func_a
的函数体,而不是执行func_a
后的返回值。runtime
函数return
的是wrapper
这个函数名,而不是wrapper()
这样被调用后的返回值。runtime
函数内部的内容,一开始它又碰到了一个函数wrapper
,wrapper
函数定义块被程序观察到后不会立刻执行,而是读入内存中(这是默认规则)。return wrapper
,返回值是个函数名,并且这个函数名会被赋值给func_a
这个被装饰的函数,也就是func_a = wrapper
。根据前面的知识,我们知道,此时func_a
函数被新的函数wrapper
覆盖了(实际上是func_a
这个函数名更改成指向wrapper
这个函数名指向的函数体内存地址,func_a
不再指向它原来的函数体的内存地址),再往后调用func_a
的时候将执行wrapper
函数内的代码,而不是先前的函数体。那么先前的函数体去哪了?还记得我们将func_a
当做参数传递给runtime
这个形参么?func
这个变量保存了老的函数在内存中的地址,通过它就可以执行老的函数体,你能在wrapper
函数里看到f = func()
这句代码,它就是这么干的!func_a()
的方式调用func_a
函数时,执行的就不再是旧的func_a
函数的代码,而是wrapper
函数的代码。在本例中,它首先会记录开始时间戳,当然了根据业务需要,显然你可以换成任意的代码,然后,它会执行func
函数并将返回值赋值给变量f
,这个func
函数就是旧的func_a
函数,执行完成之后记录结束时间戳,再打印时间间隔,最后返回f
这个变量。我们在业务需要的代码上可以用f = func_a()
的方式接受f
的值。那么为什么我们要搞一个runtime
函数一个wrapper
函数这么复杂呢?一层函数不行吗?
答:请注意,@runtime
这句代码在程序执行到这里的时候就会自动执行runtime
函数内部的代码,如果不封装一下,在业务代码还未进行调用的时候,就执行了,这和初衷不符。当然,如果你对这个有需求也不是不行。请看下面的例子,它只有一层函数。
def runtime(func):
start = time.time()
f = func() # 原函数
end = time.time()
print("运行时长:%.4f 秒" % (end-start))
return f
@runtime
def func_a():
print("love naug")
time.sleep(0.5)
if __name__ == '__main__':
func_a()
看见了吗?我们只是定义好了装饰器,业务代码还没有调用runtime
函数呢,程序就把工作全做了。这就是为什么要封装一层函数的原因。
装饰器执行顺序是否有规定呢?到底是按照我们添加语法糖的顺序来加载还是倒序的输出呢?这个是应用了队列结构还是栈的结构呢?不妨来进行下一步的实验
import time
def runtime(func):
def wrapper(*args, **kwargs):
start = time.time()
f = func(*args, **kwargs) # 原函数
time.sleep(0.5)
end = time.time()
print("运行时长:forever")
return f
return wrapper
def dating(func):
def wrapper(*args, **kwargs):
f = func(*args, **kwargs) # 原函数
print("nice time!")
return f
return wrapper
@dating
@runtime
def func_a(girl):
print("love "+girl)
@runtime
@dating
def func_b(girl):
print("love "+girl)
if __name__ == '__main__':
func_a('naug')
func_b('naug')
通过这个例子我们知道了函数是可以被多个装饰器装饰的,就像女孩子会带上各种各种漂漂亮亮的装饰品一样,我们这里要关注的是加载语法糖的顺序对执行器的影响,从demo中我们可以看到,func_a
和func_b
都是优先执行自己方法里面的逻辑,之后再执行装饰器的逻辑,越靠近def
的越先执行
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。