前言
第一次尝试写博客,记录自己的学习笔记,内容仅供参考,如有错误,欢迎指正。
第一次接触装饰器时,虽然跟随课程完成了代码,但是对装饰器还是一知半解,让我自己写装饰器还是写不出来,原因还是对装饰器理解不深刻。最近又重新复习了装饰器课程,也读了许多文章,以下是我个人对装饰器的理解笔记。
闭包
在学习装饰器之前,首先我们需要理解闭包是什么?理解闭包能够帮助我们更好的理解装饰器的原理。
闭包到底是什么呢?
简单来说 一个函数里嵌套另一个函数的定义,但是我们要用到的确是是内部函数,那么这种形式被称作闭包 。
而且内部函数会用到外部函数的变量,那么就把内部函数代码和所使用的到的外部函数变量这个整体看做一个闭包。
直接上例子
大家都知道y=kx+b这个方程是不同直线的表达式
现在我们需要根据传入的k、b、x的值求的不同直线的y值
大家可能会有不同的解决方案,比如用函数、用面向对象的方法解决
用函数方法的话,每换一条线都需要创建一个新的函数,效率不高
用面向对象的话,效果很好,但是类会附带许多不必要的属性,例如__del__、__name__等方法 会占用许多内存空间
我们需要的仅仅是k、b和x的值求出y值即可。这时候就用到我们闭包了。
先看代码
我解释一下代码运行的流程
程序是从上往下执行的,我们都知道函数只有在调用的时候才会执行,其实定义函数以后python解释器就默认做了一些事情。
当python解释器发现函数定义后,会自动分配一段内存空间给函数,并且使用变量名指向这片空间,空间里面包含函数内要执行的代(这里不知道理解的恰不恰当,有错误请大家指出来)。
第3行发现line函数的定义,会分配变量名line指向一片内存地址,内存地址中包含了函数中的代码,在当前函数中,发现了另一个函数的定义,因此又会分配create_y变量名指向另一片内存空间,内存空间中包含了print(k*x+b)的代码段。
到第7行代码调用函数,那么会带着k=1,b=2实参进入函数执行,碰到return会返回create_y的引用。return可以理解为谁调用的函数,谁就是return后面的引用,当前是第7行调用line函数,并且碰到了return,返回了内部函数create_y的引用,那么就相当于当前lin2(1,2)—>create_y,而line1–>line(1,2)
因此lin1–>create_y,相当于line1就是create_y的引用,那么第八行调用line()相当于调用create_y()方法。这点要理解。
那么执行create_y时候发现需要用到k和b变量的值,但是在create_y方法中并没有定义,因此它回去外函数中寻找是否有k、b的值,就像普通函数会去调用全局变量一样。它发现在外函数中正好定义了k和b,因此就会拿过来直接使用。现在内部函数用到了外部函数的变量,那么这个整体就叫做闭包。
思考:函数、匿名函数、闭包、对象当做实参时,有什么区别?
匿名函数只能完成基本的简单功能,传递的是函数的引用,只有功能
普通函数能完成较为复杂的功能,传递的是函数的引用,只有功能
闭包能够完成较复杂的功能,传递的是闭包中的函数以及数据,功能+数据
对象能够完成复杂的功能,传递的是很多数据和很多功能, 功能+数据
装饰器
那么究竟什么是装饰器呢?用文字说明:装饰器就是一个可以在不修改原函数代码的情况下给原函数添加新的功能的工具。有些时候,由于引用问题,我们不能对原函数直接进行修改,因此有了装饰器的概念。
概括:
1、用于拓展原来函数功能的一种函数
2、返回值也是函数的函数
3、在不用修改源代码的前提下给函数添加新的功能
实例中理解
定义一个测试函数
def test1():
print('----test1-----')
上面定义定义的函数,作用就是打印test1内容,现在我们要给他加入权限权限验证功能,通过了权限验证之后才能调用函数。
我们写代码要遵循开放封闭原则,尽量不去修改原函数。
因此我们将功能封装到另一个函数中
def test2():
print('权限验证1...')
print('权限验证2...')
test1()
好了,现在简单实现了权限验证功能。
运行一下
>权限验证1...
>权限验证2...
>----test1----
此处能够看到,我们将test1函数封装到了test2中,通过调用test2函数来实现权限验证的功能。
但是这里有一点:我们改变了调用方法。这并不是一个完美的解决方案。
接下来看这条解决方案
通过上面闭包的理解,我们需要弄清楚当前函数的指向
(这里有点绕,一定结合图片理解)
第13行set_func(test1)会携带这test1实参传入set_func函数中,那么当前func就指向test1所指向的空间,set_func碰到了return则会指向call_func所指向的空间,test1–>set_func(test1)–>call_func,这里相当于修改了test1的指向,相当于test1现在就是call_func的引用。而参数func则指向原来test1所指向的空间。
因此第14行执行,test1()就相当于执行call_func()函数,会依次执行两次print(‘权限验证’)以及调用func()方法,而func–>test1原来的空间,即包含print(’----test1----’)的空间
因此输出结果为
>权限验证1...
>权限验证2...
>----test1----
通过上面代码的同样实现了权限验证功能,功劳全归tes1=set_func(test1)这行代码,这行代码修改了原来test1函数的指向,并且用另一个函数func指向了原来test1函数。因此能够实现先权限验证,后执行的功能。这就是装饰器的原理。
但是如果每次要添加功能都需要写上这么一段代码,未免比较麻烦,不要怕,人生苦短我用python嘛,python内置@语法糖,就是为了简化装饰器的使用而存在的。
下面就是使用@语法糖实现上面功能的代码
def set_func(func):
def call_func():
print('权限验证1')
print('权限验证2')
func()
return call_func # 返回函数的引用
@set_func # 这里就等价于test1 = set_func(test1)
def test1():
print('--------test1---------')
# test1 = set_func(test1)
test1()
运行结果同样为
>权限验证1...
>权限验证2...
>----test1----
这就是@语法糖装饰器的使用,是不是很方便
理解了这一点,装饰器就差不多懂了。
下面我就直接上代码来展现不同函数进行修饰的方法了,希望大家能够自己读懂。
对没有参数没有返回值的函数进行装饰(最基础装饰器)
def set_func(func):
def call_func():
print('权限验证1')
print('权限验证2')
func()
return call_func # 返回函数的引用
@set_func # 等价于test1 = set_func(test1)
def test1():
print('--------test1---------')
# test1 = set_func(test1)
test1()
对有参数、无返回值的函数进行修饰
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---------{}'.format(num))
# test1 = set_func(test1)
test1(200)
对不定长参数函数的修饰
def set_func(func):
def call_func(*args, **kwargs):
print('权限验证1')
print('权限验证2')
# func(args, kwargs) 不可以 这里相当于传入了2个参数 一个元组 一个字典
func(*args, **kwargs) # 而这里相当于拆包
return call_func # 返回函数的引用
@set_func # 等价于test1 = set_func(test1)
def test1(num, *args, **kwargs):
print('--------test1---------{}'.format(num))
print('--------test1---------{}', args)
print('--------test1---------{}', kwargs)
# test1 = set_func(test1)
test1(200)
test1(1, 2, 4, 5, k=5, b=3)
对带有返回值的函数进行修饰-----通用装饰器
def set_func(func):
def call_func(*args, **kwargs):
print('权限验证1')
print('权限验证2')
# func(args, kwargs) 不可以 这里相当于传入了2个参数 一个元组 一个字典
return func(*args, **kwargs) # 而这里相当于拆包
return call_func # 返回函数的引用
@set_func # 等价于test1 = set_func(test1)
def test1(num, *args, **kwargs):
print('--------test1---------{}'.format(num))
print('--------test1---------{}', args)
print('--------test1---------{}', kwargs)
return 'ok'
# test1 = set_func(test1)
res = test1(200)
print(res)
多个装饰器对同一个函数修饰
def add_qx(func):
print('开始执行add_qx装饰器') # 装饰器不是在调用的时候才有作用 执行到@代码时就开始装饰了
def call_func(*args, **kwargs):
'''内部函数'''
print('权限验证')
return func(*args, **kwargs)
return call_func # 返回函数的引用
def add_xx(func):
print('开始执行add_xx装饰器')
def call_func(*args, **kwargs):
'''内部函数'''
print('log日志功能')
return func(*args, **kwargs)
return call_func # 返回函数的引用
#执行顺序为从上到下
@add_qx # 等价于test1 = set_func(test1)
@add_xx # 装饰器会对函数进行装饰 因此先执行add_xx装饰器 然后执行add_qx装饰器
def test1():
'''test1函数'''
print('---test1---')
test1()
# ------------------
开始执行add_xx装饰器
开始执行add_qx装饰器
权限验证
log日志功能
---test1---
多个装饰器对同一个函数装饰顺序demo
顺序为谁在上边先执行谁
def set_call_1(func):
def call_func():
return "<h1>" + func() + "</h1>"
return call_func
def set_call_2(func):
def call_func():
return "<span>" + func() + "</span>"
return call_func
@set_call_1
@set_call_2
def get_str():
return 'hahaha'
print(get_str())
# ---------------------
<h1><span>hahaha</span></h1>
wraps的作用(还原原函数属性)
首先介绍魔方方法__doc__ 和 __name__两个属性会输出当前函数或者对象的注释和名字
# 没有使用装饰器时,打印函数的属性
def hello():
'''简单功能模拟'''
print('hello')
if __name__ == '__main__':
hello()
print('doc--{}'.format(hello.__doc__))
print('name--{}'.format(hello.__name__))
# 输出---------------------------------
hello
doc--简单功能模拟 # hello函数的注释
name--hello # hello函数的函数名
在使用装饰器装饰之后打印会发现函数的属性已经改变为装饰器内部的函数属性
# 使用装饰器
def decorator(func):
def wrapper():
'''这是wrapper函数的注释'''
print('开始执行')
func()
print('结束执行')
return wrapper
@decorator
def hello():
'''简单功能模拟'''
print('hello')
if __name__ == '__main__':
hello()
print('doc--{}'.format(hello.__doc__))
print('name--{}'.format(hello.__name__))
# 输出----------------------------------
开始执行
hello
结束执行
doc--这是wrapper函数的注释 # 此时hello的doc属性已经变成wrapper函数的doc属性
name--wrapper # hello的name属性也变成了wrapper的name属性
在wrapper函数内部打印func的doc以及name属性 发现是hello函数的doc和name属性
def decorator(func):
def wrapper():
'''这是wrapper函数的注释'''
print('开始执行')
print('wrapper--doc--{}'.format(func.__doc__)) # 输出wrapper--doc--简单功能模拟
print('wrapper--name--{}'.format(func.__name__))# 输出wrapper--name--hello
func()
print('结束执行')
return wrapper
@decorator
def hello():
'''简单功能模拟'''
print('hello')
但是我们并不想装饰器修改原函数的属性 因此 可以在装饰器内部重新修改wrapper函数的属性为原函数属性
def decorator(func):
def wrapper():
'''这是wrapper函数的注释'''
print('开始执行')
print('wrapper--doc--{}'.format(func.__doc__))
print('wrapper--name--{}'.format(func.__name__))
func()
print('结束执行')
# 这两行的作用是将hello属性还原
wrapper.__doc__ = func.__doc__
wrapper.__name__ = func.__name__
return wrapper
@decorator
def hello():
'''简单功能模拟'''
print('hello')
if __name__ == '__main__':
hello()
print('doc--{}'.format(hello.__doc__))
print('name--{}'.format(hello.__name__))
#-----------------------------------
# 此时打印输出 便是原函数的doc以及name
开始执行
hello
结束执行
doc--简单功能模拟
name--hello
通过以上可知道装饰器会修改函数属性的指向,可以在装饰器内部通过重新赋值的方法还原
但是这种方法似乎太过繁琐,每次定义一个装饰器就需要重新修改一次 效率不高
因此引入python内置的wraps装饰器解决此问题
from functools import wraps
def decorator(func):
# 直接使用wraps装饰器再次装饰wrapper即可还原原函数属性
@wraps(func)
def wrapper():
'''这是wrapper函数的注释'''
print('开始执行')
# print('wrapper--doc--{}'.format(func.__doc__))
# print('wrapper--name--{}'.format(func.__name__))
func()
print('结束执行')
# wrapper.__doc__ = func.__doc__
# wrapper.__name__ = func.__name__
return wrapper
@decorator
def hello():
'''简单功能模拟'''
print('hello')
if __name__ == '__main__':
hello()
print('doc--{}'.format(hello.__doc__))
print('name--{}'.format(hello.__name__))
# ----------------------
开始执行
hello
结束执行
doc--简单功能模拟
name--hello
这样高效的解决了 装饰器修改函数属性指向的问题
第一次写文章,写的不好,请大家见谅
以上就是我对装饰器的理解,希望能够帮助到各位,如果有错误,请指出来谢谢。