• Python装饰器

    发布时间:2020-01-11 10:22:45
    作者:ynkulusi
  • 转自:面试攻略 | 5分钟全面掌握 Python 装饰器

    Python的装饰器是面试的常客,因为其写法复杂多变,经常忘记什么地方应该写哪种参数,新手学习起来也经常一头雾水,不怕不怕,看了这一篇你对装饰器的各种用法就全明白了。废话不多说,直接进入主题!
     

    不带参数的函数,不带参数的装饰器

    我们先来写一个简单的装饰器,实现将函数运行前后的情况记录下来。

    def dec1(func):
        print(func)
        def _wrap():
            print('before run')
            r = func()
            print('after run')
            return r
        return _wrap
    
    @dec1
    def f1():
        print('call f1')
    

    上面只是定义了两个函数,运行后发现竟然有输出:

    <function f1 at 0x7fa1585f8488>
    

    仔细看看,原来是第一个print语句的输出。这说明装饰的函数还没有实际运行的时候,装饰器就运行过了,因为@dec1相当于单独的一个语句:

    dec1(f1)
    

    那我们来正式运行一下f1吧:

    f1(1)
    

    输出如下确实达到了预期的效果:

    before run
    call f1
    after run
    

    不带参数的函数,带空参数的装饰器

    后面我们还想要给装饰器加上参数呢,先试试用这个方式调用会发生什么情况:

    @dec1()
    

    输出了错误:

    Traceback (most recent call last)
    <ipython-input-268-01cf93cf6907> in <module>
          8     return _wrap
          9 
    ---> 10 @dec1()
         11 def f1():
         12     print('call f1')
    
    TypeError: dec1() missing 1 required positional argument: 'func'
    

    它说dec1需要接受func这个参数才行,那我们改改,作为f2函数吧:

    def dec2():
        def _wrap(func):
            print(func)
            print('before run')
            return func
        return _wrap
    
    @dec2()
    def f2():
        print('call f2')
    
    f2()
    

    这下可以了:

    <function f2 at 0x7fa1585af2f0>
    before run
    call f2
    

    可是这个结构和原来有点不同了,而且,after run要写在哪里呢?很愁人地又改了一版,把它叫做f2x吧,对比dec1,又多了一层函数dec2x_w,开始有点晕:

    def dec2x_w():
        def dec2x(func):
            print(func)
            def _wrap():
                print('before run')
                r = func()
                print('after run')
                return r
            return _wrap
        return dec2x
    
    @dec2x_w()
    def f2x():
        print('call f2x')
    
    f2x()
    

    运行一下看看,确实是想要的:

    <function f2x at 0x7fa1585af950>
    before run
    call f2x
    after run
    

    后面我们就不加before/after run了。

    带参数的函数,不带参数的装饰器

    函数f2x想要接受参数呢?我们把它叫做a吧,比较简单,不就是_wrap的参数吗,加上就是了,而且又回到了只有两层函数就可以实现了:

    def dec3(func):
        print(func)
        def _wrap(param):
            print(param)
            r = func(param)
            return r
        return _wrap
    
    @dec3
    def f3(a):
        print('call f3', a)
    
    f3(1)
    

    很争气地输出了结果:

    <function f3 at 0x7fa158719620>
    1
    call f3 1
    

    带参数的函数,带参数的装饰器

    下面我们实现一个装饰器,传入一个整数,和函数传入的参数做个加法:

    def dec4_w(d_param):
        print(d_param)
        def dec4(func):
            print(func)
            def _wrap(param):
                print(param)
                r = func(param + d_param)
                return r
            return _wrap
        return dec4
    
    @dec4_w(2)
    def f4(a):
        print('call f4', a)
    
    f4(1)
    

    输出1+2=3

    2
    <function f4 at 0x7fa1585af598>
    1
    call f4 3
    

    从调用的装饰器往里看,注意这三层函数的形参,第一层是装饰器的参数,第二层是函数,第三层是函数的参数,很有规律的排列,先记一下这个规律(要考的)。带两个参数的也是一样的,接着写就可以了:

    def dec5_w(d_param_1, d_param_2):
        print(d_param_1, d_param_2)
        def dec5(func):
            print(func)
            def _wrap(param):
                print(param)
                r = func(param + d_param_1 + d_param_2)
                return r
            return _wrap
        return dec5
    
    @dec5_w(2, 3)
    def f5(a):
        print('call f5', a)
    
    f5(1)
    

    输出1+2+3=6

    2 3
    <function f5 at 0x7fa1586237b8>
    1
    call f5 6
    

    如果用不定数量的位置参数,就用*args作为形参吧:

    def dec6_w(*args):
        d_param_1, d_param_2, = args
        print(d_param_1, d_param_2)
        def dec6(func):
            print(func)
            def _wrap(*args):
                param = args[0]
                print(param)
                r = func(param + d_param_1 + d_param_2)
                return r
            return _wrap
        return dec6
    
    @dec6_w(2, 3)
    def f6(a):
        print('call f6', a)
    
    f6(1)
    print(f6.__name__)
    

    顺便输出了一下f6的函数名:

    2 3
    <function f6 at 0x7fa1586236a8>
    1
    call f6 6
    _wrap
    

    咦!怎么肥四!!!f6怎么是里面那个_wrap的名字呢?不怕不怕,functools提供了一个wraps装饰器专治各种不服(在装饰器里面放上另一个装饰器):

    from functools import wraps
    
    def dec7_w(*args):
        d_param_1, d_param_2, = args
        print(d_param_1, d_param_2)
        def dec7(func):
            print(func)
            @wraps(func)
            def _wrap(*args):
                param = args[0]
                print(param)
                r = func(param + d_param_1 + d_param_2)
                return r
            return _wrap
        return dec7
    
    @dec7_w(2, 3)
    def f7(a):
        print('call f7', a)
    
    f7(1)
    print(f7.__name__)
    

    这下正常输出f7了:

    2 3
    <function f7 at 0x7fa1585f8510>
    1
    call f7 6
    f7
    

    装饰器类(带参数的函数,带参数的装饰器)

    用函数做装饰器局限性太多了,用相同的调用方法,把函数f7改成类怎么样?emmm…改造工程有点大,直接看看成品吧:

    from functools import wraps
    
    class dec8_c:
        def __init__(self, *args):
            self.d_param_1, self.d_param_2, = args
            print(self.d_param_1, self.d_param_2)
        def  __call__(self, func):
            print(func)
            @wraps(func)
            def _wrap(param):
                print(param)
                r = func(param + self.d_param_1 + self.d_param_2)
                return r
            return _wrap
    
    @dec8_c(2, 3)
    def f8(a):
        print('call f8', a)
    
    f8(1)
    print(f8.__name__)
    

    看看是不是实现了一样的效果:

    2 3
    <function f8 at 0x7fa1585f8048>
    1
    call f8 6
    f8
    

    虽然使用了__call__,但这里的__init__不能省略(因为它需要知道参数个数),否则会出现这个错误:

    Traceback (most recent call last)
    <ipython-input-276-1634a47057a2> in <module>
         14         return dec8
         15 
    ---> 16 @dec8_c(2, 3)
         17 def f8(a):
         18     print('call f8', a)
    
    TypeError: dec8_c() takes no arguments
    

    同时还可以发现,__call__只需要两层函数了,去掉了第二层,直接把_wrap的函数体往上提了一层!

    装饰器类(带参数的函数,不带参数的装饰器)

    大概是吃饱了撑着,又想要实现一开始那个不带参数的装饰器了,那就继续敲敲打打一番看看:

    class dec9_c:
        def  __init__(self, func):
            print(func)
            self.func = func
            self.__name__ = func.__name__
        def  __call__(self, param):
            print(param)
            func = self.func
            r = func(param)
            return r
    
    @dec9_c
    def f9(a):
        print('call f9', a)
    
    f9(1)
    print(f9.__name__)
    

    赶快运行看看:

    <function f9 at 0x7fa1585f8730>
    1
    call f9 1
    f9
    

    咦,f9的函数名可以直接打印,这下都不用@wraps了呢!呃,再仔细看看,这写法好像有些不一样啊:

    • dec8_cinit带的是装饰器的参数,可是dec9_c带的是装饰器函数自己!

    • 所以实际调用的函数名也可以在init中传给它了哦!

    • 而且call函数也简洁了很多,看来有没有参数真的有很大区别呢!

    这里先做个总结,装饰器使用函数名形式(不带括号)和使用函数调用形式(带括号和参数)在实现上是不同的,因为前者是函数本身,而后者是从装饰器函数中返回的函数。这也是f2相比f1缺少了记录after run的原因,因为dec1直接调用了f2,而dec2先运行得到函数,再把函数返回去调用f2。用装饰器类就可以解决这个问题,因为它是对__call__的调用,只需要自己定义一下就可以了。

    上面的f9要写两个函数,能不能写得和f1一样简洁?当然是可以的,使用__new__大法:

    from functools import wraps
    
    class dec9x_c:
        def  __new__(self, func):
            print(func)
            @wraps(func)
            def dec9x(param):
                print(param)
                r = func(param)
                return r
            return dec9x
    
    @dec9x_c
    def f9x(a):
        print('call f9x', a)
    
    f9x(1)
    print(f9x.__name__)
    

    这样就避开了函数调用,不用打call了(定义__call__函数),快看它来了:

    <function f9x at 0x7fa158623bf8>
    1
    call f9x 1
    f9x
     
  • 分类:python
    标签: python decorator
    评论数:0 阅读数:897