修饰器,是python 函数调用的一种简记。通过使用修饰器,我们可以方便的修改其他函数的功能。
入门
正常情况,我们考虑修改一个函数,增加函数的功能,可以考虑使用以下的写法。
def wrapped_fun(fun):
def wrapped():
before_fun() # 函数执行之前的一些操作
fun()
after_fun() # 函数执行之后的操作
return wrapped
在这里,我们写了一个简单的函数,它接受一个函数,并且在函数执行的前后增加了一些额外的操作。假设有函数def fun1()
我们可以使用fun1 = wrapped_fun(fun1)
对函数进行扩充。python 提供注解@来简化fun1 = wrapped_fun(fun1)
的写法。这种简化的写法,被称为修饰器,使用方法如下
@wrapped_fun
def fun1():
pass
上述操作等价于
def fun1():
pass
fun1 = wrapped_fun(fun1)
在函数加了注解 @wrapped_fun
后再执行函数, fun1()
就相当于执行了 wrapped_fun(fun1)()
。
例一:在函数执行前后输出语句
def before_fun(fun):
print(1)
def wrapped():
print("函数执行之前运行")
fun()
return wrapped
def after_fun(fun):
print(2)
def wrapped():
fun()
print("函数执行之后执行")
return wrapped
@before_fun
@after_fun
def main_fun():
print("主函数执行·了·")
if __name__ == "__main__":
main_fun() # 等价于 before_fun(after_fun(main_fun))(),执行顺序为after_fun先,before_fun后
案例输出:
2
1
函数执行之前运行
主函数执行·了·
函数执行之后执行
例二:有参函数的修饰
函数是可以传参的,python 中使用def fun(*arg, **kwargs)
,来表示一般函数的参数, 其中*arg
表示tuple, 是任意多个无名参数。而**kwargs
表示键值的有名参数,输出为一个字典,例如
def fun(*args, **kwargs):
print(args)
print(kwargs)
fun(1,23,'a',name="fun", a='a', b='b')
输出结果为
(1, 23, 'a')
{'name': 'fun', 'a': 'a', 'b': 'b'}
有了上述的知识,我们只需要改变wrapped_fun
中函数wrapped
的传参,就可以实现有参函数的修饰了
def wrapped_print_name(fun):
def wrapped(*args, **kwargs):
print("名字是", end=":")
fun(*args, **kwargs)
return wrapped
@wrapped_print_name
def print_name(name):
print(name)
if __name__ == "__main__":
print_name("小明")
输出结果为
名字是:小明
例三:修饰器传参
有时候,我们希望修把参数写到修饰器里,而不是函数里(虽然他们可以实现一样的效果),随着传入参数不同,修饰器行为也不同。我们可以使用修饰器的嵌套来完成。或者说,我只需要执行一个可以接受变量的函数,让他返回一个修饰器,就实现了修饰器随参数改变。
def deco(say):
def wrapped_print_everything(fun):
def wrapped(*args, **kwargs):
print(say, end=":")
fun(*args, **kwargs)
return wrapped
return wrapped_print_everything
@deco("名字是")
def print_name(name):
print(name)
@deco("属性是")
def print_type(name):
print(name)
if __name__ == "__main__":
print_name("小明")
print_type("高富帅")
输出结果
名字是:小明
属性是:高富帅
例四:@functools.wraps()
@functools.wraps()
注解可以让函数在被修饰器修饰时,只改变函数功能。而不改变函数其他属性。在入门中我们说到,修饰器的本质是把函数经过另一个函数的加工,也即fun1 = wrapped_fun(fun1)
的形式。那么尝试执行如下案例。
def deco(func):
def wrapped(*args, **kwargs):
func(*args, **kwargs)
return wrapped
def func1():
pass
@deco
def func2():
pass
print(func1)
print(func2)
输出如下,可见加入了修饰器的func2
已经执行了fun2 = wrapped(fun2)
,所以在打印函数名字的时候就会有如下的输出。
<function func1 at 0x000001C33FAD2AF0>
<function deco.<locals>.wrapped at 0x000001C342412700>
但这从调用者的角度上来看非常违反常识。同样是函数,fun1
可以打印时显示的是自己的名字,为什么函数是fun2
输出的却是一个奇怪的wrapped
呢? 这时@functools.wraps()
就被发明了出来解决这个问题,使用@functools.wraps()
,在使用修饰器后,仍然可以返回原来的函数名。
import functools
def deco(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
func(*args, **kwargs)
return wrapped
def func1():
pass
@deco
def func2():
pass
print(func1)
print(func2)
输出结果
<function func1 at 0x0000027FF5B72AF0>
<function func2 at 0x0000027FF84C2700>
面试题:阅读代码,并写出输出
def deco(name):
print(name)
return deco
@deco("1")
def fun():
print('2')
fun('3')
【 答案 】
1
<function fun at 0x...>
3
【 解析 】
def deco(name):
print(name)
return deco
等价于
def deco(name):
print(name)
def deco1(name):
print(name)
return deco1
return deco1
这是一个嵌套的逻辑,套用示例三,这是一个修饰器传参,可以输出参数“1” 后可以去除最外层嵌套,转化为
def deco1(name):
print(name)
return deco1
@deco1
def fun():
print('2')
再将修饰器 @deco1 进行展开,上式等价于
fun = deco1(fun)
也就是说,当我们最后执行了fun('3')
,会执行deco1(fun)('3')
,观察deco1函数,这个一个返回自己本身的函数,有的小伙伴可能以为这个是递归。其实不是,这个函数只是返回了自己,而递归需要调用自己。如果是递归,他应该写成下面的样子。
def deco1(name):
print(name)
return deco1(name)
所以他只返回了自己本身,我们可以做个测试,对于上式deco1(fun)('3')
,可以写成两步a = deco1(fun)
和 a('3')
,代码如下。
def deco1(name):
print(name)
return deco1
def fun():
print('2')
a = deco1(fun)
print("-------------a:", a)
b = a('3')
print("-------------b:", b)
结果
<function fun at 0x00000239CAA12AF0>
-------------a: <function deco1 at 0x00000239CA65D1F0>
3
-------------b: <function deco1 at 0x00000239CA65D1F0>
可以看到a
和b
都是function deco1
,得到这样的输出,在反观demo1
这个函数的作用就是调用以后,还可以被调用,可以无限的链式嵌套下去。例如我们可以写表达式deco1("1")("2")("3")("4")("5")
,他就可以在控制台输出12345。我们还可以继续加括号调用。所以执行deco1(fun)('3')
会在控制台打印出一个函数<function fun at 0x0000027FF5B72AF0> 和 3,再加上之前嵌套的一层修饰器传参输出的 1,一共三个输出。由于全程中没有调用fun
,只是使用了他的名字,所以print('2')
不会被执行。
22-01-08:更改面试题中的错误。
22-01-11:增加例一,输出顺序。