python装饰器原理源码示例分析
作者:攻城狮白玉 时间:2022-03-25 18:31:04
前言
最近有人问我装饰器是什么,我就跟他说,其实就是装饰器就是类似于女孩子的发卡。你喜欢的一个女孩子,她可以有很多个发卡,而当她戴上不同的发卡,她的头顶上就是装饰了不同的发卡。但是你喜欢的女孩子还是你喜欢的女孩子。如果还觉得不理解的话,装饰器就是咱们的手机壳,你尽管套上了手机壳,但并不影响你的手机功能,可你的手机还是该可以给你玩,该打电话打电话,该玩游戏玩游戏,该收藏攻城狮白玉的博客就收藏攻城狮白玉的博客。而你的手机就变成了带手机壳的手机。
装饰器就是python的一个拦路虎,你干或者不 * ,它都在那里。如果你想学会高级的python用法,装饰器就是你这个武松必须打倒的一只虎
本文的环境如下:
win10
python3.7
一、什么是装饰器
装饰器是给现有的模块增添新的小功能,可以对原函数进行功能扩展,而且还不需要修改原函数的内容,也不需要修改原函数的调用。
装饰器的使用符合了面向对象编程的开放封闭原则。
开放封闭原则主要体现在两个方面:
对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。
二、为什么要用装饰器
使用装饰器之前,我们要知道,其实python里是万物皆对象,也就是万物都可传参。
函数也可以作为函数的参数进行传递的。
通过下面这个简单的例子可以更直观知道函数名是如何直接作为参数进行传递
def baiyu():
print("我是攻城狮白玉")
def blog(name):
print('进入blog函数')
name()
print('我的博客是 https://blog.csdn.net/zhh763984017')
if __name__ == '__main__':
func = baiyu # 这里是把baiyu这个函数名赋值给变量func
func() # 执行func函数
print('------------')
blog(baiyu) # 把baiyu这个函数作为参数传递给blog函数
执行结果如下所示:
接下来,我想知道这baiyu
和blog
两个函数分别的执行时间是多少,我就把代码修改如下:
import time
def baiyu():
t1 = time.time()
print("我是攻城狮白玉")
time.sleep(2)
print("执行时间为:", time.time() - t1)
def blog(name):
t1 = time.time()
print('进入blog函数')
name()
print('我的博客是 https://blog.csdn.net/zhh763984017')
print("执行时间为:", time.time() - t1)
if __name__ == '__main__':
func = baiyu # 这里是把baiyu这个函数名赋值给变量func
func() # 执行func函数
print('------------')
blog(baiyu) # 把baiyu这个函数作为参数传递给blog函数
上述的改写已经实现了我需要的功能,但是,当我有另外一个新的函数【python_blog_list】,具体如下:
def python_blog_list():
print('''【Python】爬虫实战,零基础初试爬虫下载图片(附源码和分析过程)
https://blog.csdn.net/zhh763984017/article/details/119063252 ''')
print('''【Python】除了多线程和多进程,你还要会协程
https://blog.csdn.net/zhh763984017/article/details/118958668 ''')
print('''【Python】爬虫提速小技巧,多线程与多进程(附源码示例)
https://blog.csdn.net/zhh763984017/article/details/118773313 ''')
print('''【Python】爬虫解析利器Xpath,由浅入深快速掌握(附源码例子)
https://blog.csdn.net/zhh763984017/article/details/118634945 ''')
也需要计算函数执行时间的,那按之前的逻辑,就是改写如下:
def python_blog_list():
t1 = time.time()
print('''【Python】爬虫实战,零基础初试爬虫下载图片(附源码和分析过程)
https://blog.csdn.net/zhh763984017/article/details/119063252 ''')
print('''【Python】除了多线程和多进程,你还要会协程
https://blog.csdn.net/zhh763984017/article/details/118958668 ''')
print('''【Python】爬虫提速小技巧,多线程与多进程(附源码示例)
https://blog.csdn.net/zhh763984017/article/details/118773313 ''')
print('''【Python】爬虫解析利器Xpath,由浅入深快速掌握(附源码例子)
https://blog.csdn.net/zhh763984017/article/details/118634945 ''')
print("执行时间为:", time.time() - t1)
如果也要这样子写的话,不就重复造轮子了吗?虽说人类的本质是鸽王和复读机,但作为一个优秀的cv攻城狮(ctrl+c和ctrl+v)肯定是要想办法偷懒的呀
装饰器,就是可以让我们拓展一些原有函数没有的功能。
三、简单的装饰器
基于上面的函数执行时间的需求,我们就手写一个简单的装饰器进行实现。
import time
def baiyu():
print("我是攻城狮白玉")
time.sleep(2)
def count_time(func):
def wrapper():
t1 = time.time()
func()
print("执行时间为:", time.time() - t1)
return wrapper
if __name__ == '__main__':
baiyu = count_time(baiyu) # 因为装饰器 count_time(baiyu) 返回的时函数对象 wrapper,这条语句相当于 baiyu = wrapper
baiyu() # 执行baiyu()就相当于执行wrapper()
这里的count_time是一个装饰器,装饰器函数里面定义一个wrapper函数,把func这个函数当作参数传入,函数实现的功能是把func包裹起来,并且返回wrapper函数。wrapper函数体就是要实现装饰器的内容。
当然,这里的wrapper函数名是可以自定义的,只要你定义的函数名,跟你return的函数名是相同的就好了
四、装饰器的语法糖
你如果看过其他python项目里面的代码里,难免会看到@符号,这个@符号就是装饰器的语法糖。因此上面简单的装饰器还是可以通过语法糖来实现的,这样就可以省去
baiyu = count_time(baiyu)
这一句代码,而直接调用baiyu()这个函数
换句话说,其实使用装饰器的是,默认传入的参数就是被装饰的函数。
import time
def count_time(func):
def wrapper():
t1 = time.time()
func()
print("执行时间为:", time.time() - t1)
return wrapper
@count_time
def baiyu():
print("我是攻城狮白玉")
time.sleep(2)
if __name__ == '__main__':
# baiyu = count_time(baiyu) # 因为装饰器 count_time(baiyu) 返回的时函数对象 wrapper,这条语句相当于 baiyu = wrapper
# baiyu() # 执行baiyu()就相当于执行wrapper()
baiyu() # 用语法糖之后,就可以直接调用该函数了
五、装饰器传参
当我们的被装饰的函数是带参数的,此时要怎么写装饰器呢?
上面我们有定义了一个blog函数是带参数的
def blog(name):
print('进入blog函数')
name()
print('我的博客是 https://blog.csdn.net/zhh763984017')
此时我们的装饰器函数要优化一下下,修改成为可以接受任意参数的装饰器
def count_time(func):
def wrapper(*args,**kwargs):
t1 = time.time()
func(*args,**kwargs)
print("执行时间为:", time.time() - t1)
return wrapper
此处,我们的wrapper函数的参数为*args和**kwargs,表示可以接受任意参数
这时我们就可以调用我们的装饰器了。
import time
def count_time(func):
def wrapper(*args, **kwargs):
t1 = time.time()
func(*args, **kwargs)
print("执行时间为:", time.time() - t1)
return wrapper
@count_time
def blog(name):
print('进入blog函数')
name()
print('我的博客是 https://blog.csdn.net/zhh763984017')
if __name__ == '__main__':
# baiyu = count_time(baiyu) # 因为装饰器 count_time(baiyu) 返回的时函数对象 wrapper,这条语句相当于 baiyu = wrapper
# baiyu() # 执行baiyu()就相当于执行wrapper()
# baiyu() # 用语法糖之后,就可以直接调用该函数了
blog(baiyu)
六、带参数的装饰器
前面咱们知道,装饰器函数也是函数,既然是函数,那么就可以进行参数传递,咱们怎么写一个带参数的装饰器呢?
前面咱们的装饰器只是实现了一个计数,那么我想在使用该装饰器的时候,传入一些备注的msg信息,怎么办呢?咱们一起看一下下面的代码
import time
def count_time_args(msg=None):
def count_time(func):
def wrapper(*args, **kwargs):
t1 = time.time()
func(*args, **kwargs)
print(f"[{msg}]执行时间为:", time.time() - t1)
return wrapper
return count_time
@count_time_args(msg="baiyu")
def fun_one():
time.sleep(1)
@count_time_args(msg="zhh")
def fun_two():
time.sleep(1)
@count_time_args(msg="mylove")
def fun_three():
time.sleep(1)
if __name__ == '__main__':
fun_one()
fun_two()
fun_three()
咱们基于原来的count_time函数外部再包一层用于接收参数的count_time_args,接收回来的参数就可以直接在内部的函数里面调用了。上述代码执行效果如下:
七、类装饰器
上面咱们一起学习了怎么写装饰器函数,在python中,其实也可以同类来实现装饰器的功能,称之为类装饰器。类装饰器的实现是调用了类里面的__call__函数。类装饰器的写法比我们装饰器函数的写法更加简单。
当我们将类作为一个装饰器,工作流程:
通过__init__()方法初始化类
通过__call__()方法调用真正的装饰方法
import time
class BaiyuDecorator:
def __init__(self, func):
self.func = func
print("执行类的__init__方法")
def __call__(self, *args, **kwargs):
print('进入__call__函数')
t1 = time.time()
self.func(*args, **kwargs)
print("执行时间为:", time.time() - t1)
@BaiyuDecorator
def baiyu():
print("我是攻城狮白玉")
time.sleep(2)
def python_blog_list():
time.sleep(5)
print('''【Python】爬虫实战,零基础初试爬虫下载图片(附源码和分析过程)
https://blog.csdn.net/zhh763984017/article/details/119063252 ''')
print('''【Python】除了多线程和多进程,你还要会协程
https://blog.csdn.net/zhh763984017/article/details/118958668 ''')
print('''【Python】爬虫提速小技巧,多线程与多进程(附源码示例)
https://blog.csdn.net/zhh763984017/article/details/118773313 ''')
print('''【Python】爬虫解析利器Xpath,由浅入深快速掌握(附源码例子)
https://blog.csdn.net/zhh763984017/article/details/118634945 ''')
@BaiyuDecorator
def blog(name):
print('进入blog函数')
name()
print('我的博客是 https://blog.csdn.net/zhh763984017')
if __name__ == '__main__':
baiyu()
print('--------------')
blog(python_blog_list)
八、带参数的类装饰器
当装饰器有参数的时候,__init__() 函数就不能传入func(func代表要装饰的函数)了,而func是在__call__函数调用的时候传入的。
class BaiyuDecorator:
def __init__(self, arg1, arg2): # init()方法里面的参数都是装饰器的参数
print('执行类Decorator的__init__()方法')
self.arg1 = arg1
self.arg2 = arg2
def __call__(self, func): # 因为装饰器带了参数,所以接收传入函数变量的位置是这里
print('执行类Decorator的__call__()方法')
def baiyu_warp(*args): # 这里装饰器的函数名字可以随便命名,只要跟return的函数名相同即可
print('执行wrap()')
print('装饰器参数:', self.arg1, self.arg2)
print('执行' + func.__name__ + '()')
func(*args)
print(func.__name__ + '()执行完毕')
return baiyu_warp
@BaiyuDecorator('Hello', 'Baiyu')
def example(a1, a2, a3):
print('传入example()的参数:', a1, a2, a3)
if __name__ == '__main__':
print('准备调用example()')
example('Baiyu', 'Happy', 'Coder')
print('测试代码执行完毕')
建议各位同学好好比对一下这里的代码和不带参数的装饰器代码的区别,加深理解。
九、装饰器的顺序
一个函数可以被多个装饰器进行装饰,那么装饰器的执行顺序是怎么样的呢?咱们执行一下下面的代码就清楚了
def BaiyuDecorator_1(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
print('我是装饰器1')
return wrapper
def BaiyuDecorator_2(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
print('我是装饰器2')
return wrapper
def BaiyuDecorator_3(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
print('我是装饰器3')
return wrapper
@BaiyuDecorator_1
@BaiyuDecorator_2
@BaiyuDecorator_3
def baiyu():
print("我是攻城狮白玉")
if __name__ == '__main__':
baiyu()
由输出结果可知,在装饰器修饰完的函数,在执行的时候先执行原函数的功能,然后再由里到外依次执行装饰器的内容。
我们带三个装饰器的函数的代码如下:
@BaiyuDecorator_1
@BaiyuDecorator_2
@BaiyuDecorator_3
def baiyu():
print("我是攻城狮白玉")
上述的代码可以看作如下代码,就能理解为何是由里到外执行了
baiyu = BaiyuDecorator_1 (BaiyuDecorator_2 (BaiyuDecorator_3(baiyu)))
来源:https://blog.csdn.net/zhh763984017/article/details/120072425