【python 修饰器】入门,使用案例,面试题

修饰器,是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>

可以看到ab都是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:增加例一,输出顺序。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值