《最值得收藏的python3语法汇总》之装饰器

    技术2022-07-11  142

    目录

    关于这个系列

    1、概念和原理

    2、类装饰器

    3、内置装饰器

    @property

    @staticmethod

    @classmethod

    @abstractmethod


     

    关于这个系列

    《最值得收藏的python3语法汇总》,是我为了准备公众号“跟哥一起学python”上面视频教程而写的课件。整个课件将近200页,10w字,几乎囊括了python3所有的语法知识点。

    你可以关注这个公众号“跟哥一起学python”,获取对应的视频和实例源码。

    这是我和几位老程序员一起维护的个人公众号,全是原创性的干货编程类技术文章,欢迎关注。


    1、概念和原理

    如果你想对一个已有函数的功能进行扩展,而又不想修改这个函数,那么装饰器(decorator)就派上用场了。

    比如,我们想对一个函数打点,测试其运行时间。如果不使用装饰器,我们可以这样实现:

     

    #  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python # file: ./14/14_1.py import time # 性能打点 def foo1():     t0 = time.perf_counter()     time.sleep(1)     t1 = time.perf_counter()     print(f'time spend: {t1 - t0} s') def foo2():     t0 = time.perf_counter()     time.sleep(2)     t1 = time.perf_counter()     print(f'time spend: {t1 - t0} s') foo1() foo2()

    输出为:

    time spend: 0.9993565999999999 s

    time spend: 2.0009466 s

    可以看到,我们会给每个需要打点的函数加上计时打点的一段代码。这样写代码会带来两个问题:

    打点功能本身不是该函数的核心功能,某种意义上它只是一个开发阶段的测试代码。这是对核心代码的一种污染。打点功能是一个通用功能,如果我们在每个函数里面重复写这样一段代码,会显得很冗余。

     

    这种情况下,装饰器是一个非常好的选择。代码如下:

    #  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python # file: ./14/14_2.py import time # 性能打点 - 装饰器方式 def perf_dot(func):     def inner():         t0 = time.perf_counter()         func()         t1 = time.perf_counter()         print(f'time spend: {t1 - t0} s')     return inner @perf_dot def foo1():     time.sleep(1) @perf_dot def foo2():     time.sleep(2) foo1() foo2()

    输出为:

    time spend: 1.000558 s

    time spend: 2.000056 s

    代码里面的@perf_dot,就是装饰器。我们将打点功能封装到一个函数perf_dot中,我们只需要在被测试函数头加上这个装饰器声明即可。

     

    下面我们来看看装饰器的具体原理。

    Perf_dot是一个两层嵌套的函数,在最后是return了内层函数对象,这是不是和我们前面讲的闭包很像?没错,它其实就是一个闭包(本实例没有引用外层变量,所以__closure__里面没有值)。

    Perf_dot函数有一个入参func,这个入参其实就是这个装饰器要装饰的那个函数,比如foo1。

    当解释器解析到@perf_dot这一句时,会将下一行的函数名作为实参传给perf_dot(foo1)。而这个函数会返回自己的内层函数对象,解释器将返回的对象赋值给foo1。

    所以,当我们调用foo1()时,我们实际执行的是perf_dot的内层函数对象。我们可以打出foo1的名字:

    print(foo1.__name__)

    输出为:

    Inner

     

    解释器将我们要调用的函数,替换成了装饰器内层函数。所谓的功能扩展,就是在这个内层函数里面做文章。比如我们这个实例,在内层函数里面增加了打点功能,并且调用了真正的foo1函数功能。

    这就是装饰器的本质,是不是挺简单?

     

    下面我们对这个例子稍微修改一下,让函数带上参数:

    import time # 性能打点 - 装饰器方式 def perf_dot(func):     def inner(intv):         t0 = time.perf_counter()         func(intv)         t1 = time.perf_counter()         print(f'time spend: {t1 - t0} s')     return inner @perf_dot def foo1(intv):     time.sleep(intv) @perf_dot def foo2(intv):     time.sleep(intv) foo1(1) foo2(2)

    只要装饰器内层函数的形参列表和foo的保持一致就可以。当我们调用foo1(param…)时,我们实际调用的是inner(param…)。

     

    那么装饰器是否可以带参数呢?答案是肯定的。

    #  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python # file: ./14/14_2.py import time # 性能打点 - 装饰器方式 def perf_dot(name):     def outer(func):         def inner(intv):             t0 = time.perf_counter()             func(intv)             t1 = time.perf_counter()             print(f'{name} time spend: {t1 - t0} s')         return inner     return outer @perf_dot('foo1') def foo1(intv):     time.sleep(intv) @perf_dot('foo2') def foo2(intv):     time.sleep(intv) foo1(1) foo2(2)

    输出为:

    foo1 time spend: 1.0001801000000001 s

    foo2 time spend: 2.0004163999999998 s

    这个理解起来就稍微有点困难了。如果装饰器本身带有参数,那么装饰器函数是三层嵌套的。当执行到@perf_dot(‘foo1’)时,返回outer函数对象,它相当于不带参数的@outer。而我们传入的参数,则被封装在了闭包__closure__中。

     

    装饰器甚至可以支持嵌套,也就是一个函数多个装饰器。比如我们希望在打点之前打印一行字符,那么我们新增一个装饰器。

    #  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python # file: ./14/14_3.py import time # 装饰器嵌套 def perf_dot_log(func):     def inner():         print('start dot...')         func()         print('finish dot...')     return inner def perf_dot(func):     def inner():         t0 = time.perf_counter()         func()         t1 = time.perf_counter()         print(f'time spend: {t1 - t0} s')     return inner @perf_dot_log @perf_dot def foo1():     time.sleep(1) @perf_dot_log @perf_dot def foo2():     time.sleep(2) foo1() foo2()

    输出为:

    start dot...

    time spend: 1.0003571 s

    finish dot...

    start dot...

    time spend: 2.0000216999999996 s

    finish dot...

    我们知道,@perf_dot装饰器是将foo1替换为了它自己的内层函数对象inner。@perf_dot_log其实就是对这个inner函数再封装一层装饰器。所以foo1最终执行的是perf_dot_log的inner函数对象。它最终的调用关系是:perf_dot_log.inner -> perf_dot.inner -> foo1。

     

    2、类装饰器

    上一节我们是使用闭包函数来实现装饰器,我们也可以使用类来实现装饰器,叫做类装饰器。类装饰器需要实现一个特殊的方法__call__。

    上面的打点测试的例子,我们可以采用类装饰器来实现,如下:

    #  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python # file: ./14/14_4.py import time # 性能打点 - 类装饰器 class PerfDot:     def __init__(self, func):         self.func = func     def __call__(self):         t0 = time.perf_counter()         self.func()         t1 = time.perf_counter()         print(f'time spend: {t1 - t0} s') @PerfDot def foo1():     time.sleep(1) @PerfDot def foo2():     time.sleep(2) foo1() foo2()

    当执行到@PerfDot时,解释器会自动实例化一个PerfDot的实例对象,实例化入参就是foo1这个函数对象。解释器再把这个实例对象赋值给foo1。所以,我们调用foo1(),实质上是在执行PerfDot的一个实例对象,而不是foo1这个函数对象。

     

    再来看看__call__这个特殊方法,它的作用是让一个对象成为可调用对象(callable object)。如果一个对象后面可以跟一对括号(),那么这个对象就是可被调用的,比如函数对象。你也可以使用callable(obj)方法来判断一个对象是否是可调用的。

    Python比较神奇的一点是,它可以通过在类里面实现__call__方法,从而使得这个类的实例对象可以被直接调用,看起来就像在调用函数一样。

     

    类装饰器等同于如下的实例对象调用:

    inst1 = PerfDot(foo1) inst1()

    效果和@PerfDot是一样的。

     

    类装饰器可以更方便的携带参数,你只需要在构造方法或者__call__方法中修改形参列表即可。我觉得它理解起来比函数装饰器要容易一些。

     

    3、内置装饰器

    Python3内置了一些装饰器,用于实现一些常用的功能,下面我们介绍一些常用的内置装饰器。

     

    @property

    将get/set/delete方法转换为属性调用。

     

    #  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python # file: ./14/14_5.py # @property class Dog:     def __init__(self, name):         self.name = name         self.__age = 0     @property     def age(self):         return self.__age     @age.setter     def age(self, age):         self.__age = age     @age.deleter     def age(self):         del self.__age my_dog = Dog('Apple') my_dog.age = 5 print(f'age: {my_dog.age}') del my_dog.age

    我们可以像属性引用一样调用对应的方法。当我们使用@property装饰一个方法func之后,它会自动产生两个装饰器@func.setter和@func.deleter,他们用于装饰对应的set方法和delete方法。

     

     

    @staticmethod

    将类中的方法装饰为静态方法。所谓静态方法,就是通过类直接调用的方法。它不需要创建对象,不会隐式传递self。

     

    @classmethod

    将类中的方法装饰为类方法。所谓类方法,就是方法入参中的self是类本身,类方法只能对类变量进行操作。

     

    @abstractmethod

    将类中的方法装饰为抽象方法。含抽象方法的类不能实例化,继承了含抽象方法的子类必须重写所有抽象方法,非抽象方法可以不重写。

     

    我们通过下面的例子介绍以上3个装饰器的应用:

    #  author: Tiger,   关注公众号“跟哥一起学python”,ID:tiger-python # file: ./14/14_6.py from abc import ABCMeta, abstractmethod # 静态方法、类方法、抽象方法 class ParentClass(metaclass=ABCMeta):     desp = 'hello'     def __init__(self, name):         self.name = name         self.age = 0     @abstractmethod     def func_abstract(self):         pass     @staticmethod     def func_static(a, b):         return a + b     @classmethod     def func_class(cls, desp):         cls.desp = desp class ChildClass(ParentClass):     def func_abstract(self):         print('i\'m abstract method!') inst1 = ChildClass('Apple') inst1.func_abstract() inst1.func_class('welcome!') print(inst1.desp) print(ParentClass.func_static(1, 2)) print(ChildClass.func_static(1, 2))

     

    大家注意,这里的使用到了metaclass,它是python的元类,元类是构造类对象的类,用得比较少所以我们在前面的面向对象编程中没有讲,大家感兴趣可以自行学习。

     

     

     

     

     

    Processed: 0.013, SQL: 9